mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
Migrate ImageResizer to WinUI3 (#45288)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Migrate WPF/WinForms utility to WinUI3 can give us many benefit. 1. Only WinUI3 support AOT. By this change, we can remove the blocker to make imageResizer publish with AOT enabled to improve the performance. Through the previous testing in CmdPal, it can improve about 1.5x to 3x perf. 2. WinUI 3 provides a modern UI and makes sure that our experiences fit in with the Windows 11 look and feel. 3. We can merge many redundant code to the same one and reduce more codebase and installed size in the future if we successfully migrate all remaining WPF/WinForms utility to WinUI3. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46465 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed 1. Set up the ImageResizer as the startup project. 2. Start in visual studio. --------- Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com> Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
.github/actions/spell-check/expect.txt
vendored
3
.github/actions/spell-check/expect.txt
vendored
@@ -52,6 +52,7 @@ Apm
|
|||||||
APPBARDATA
|
APPBARDATA
|
||||||
APPEXECLINK
|
APPEXECLINK
|
||||||
APPLICATIONFRAMEHOST
|
APPLICATIONFRAMEHOST
|
||||||
|
apphost
|
||||||
appmanifest
|
appmanifest
|
||||||
APPMODEL
|
APPMODEL
|
||||||
APPNAME
|
APPNAME
|
||||||
@@ -1329,6 +1330,7 @@ rundll
|
|||||||
rungameid
|
rungameid
|
||||||
RUNLEVEL
|
RUNLEVEL
|
||||||
runtimeclass
|
runtimeclass
|
||||||
|
runtimeconfig
|
||||||
runtimepack
|
runtimepack
|
||||||
ruuid
|
ruuid
|
||||||
rvm
|
rvm
|
||||||
@@ -1583,6 +1585,7 @@ TILEDWINDOW
|
|||||||
TILLSON
|
TILLSON
|
||||||
timedate
|
timedate
|
||||||
timediff
|
timediff
|
||||||
|
timespan
|
||||||
timeutil
|
timeutil
|
||||||
TITLEBARINFO
|
TITLEBARINFO
|
||||||
Titlecase
|
Titlecase
|
||||||
|
|||||||
@@ -141,13 +141,13 @@
|
|||||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
||||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
|
"WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
|
||||||
|
|
||||||
"PowerToys.ImageResizer.exe",
|
"WinUI3Apps\\PowerToys.ImageResizer.exe",
|
||||||
"PowerToys.ImageResizer.dll",
|
"WinUI3Apps\\PowerToys.ImageResizer.dll",
|
||||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
||||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
||||||
"PowerToys.ImageResizerExt.dll",
|
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
|
||||||
"PowerToys.ImageResizerContextMenu.dll",
|
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll",
|
||||||
"ImageResizerContextMenuPackage.msix",
|
"WinUI3Apps\\ImageResizerContextMenuPackage.msix",
|
||||||
|
|
||||||
"PowerToys.LightSwitchModuleInterface.dll",
|
"PowerToys.LightSwitchModuleInterface.dll",
|
||||||
"LightSwitchService\\PowerToys.LightSwitchService.exe",
|
"LightSwitchService\\PowerToys.LightSwitchService.exe",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<!-- Resource directories should be added only if the installer is built on the build farm -->
|
<!-- Resource directories should be added only if the installer is built on the build farm -->
|
||||||
<?ifdef env.IsPipeline?>
|
<?ifdef env.IsPipeline?>
|
||||||
<?foreach ParentDirectory in INSTALLFOLDER;WinUI3AppsInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
|
<?foreach ParentDirectory in INSTALLFOLDER;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
|
||||||
<DirectoryRef Id="$(var.ParentDirectory)">
|
<DirectoryRef Id="$(var.ParentDirectory)">
|
||||||
<!-- Resource file directories -->
|
<!-- Resource file directories -->
|
||||||
<?foreach Language in $(var.LocLanguageList)?>
|
<?foreach Language in $(var.LocLanguageList)?>
|
||||||
@@ -171,12 +171,6 @@
|
|||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
<File Id="FancyZonesEditor_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.FancyZonesEditor.resources.dll" />
|
<File Id="FancyZonesEditor_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.FancyZonesEditor.resources.dll" />
|
||||||
</Component>
|
</Component>
|
||||||
<Component Id="ImageResizer_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Guid="$(var.CompGUIDPrefix)02">
|
|
||||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
|
||||||
<RegistryValue Type="string" Name="ImageResizer_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
|
||||||
</RegistryKey>
|
|
||||||
<File Id="ImageResizer_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\WinUI3Apps\$(var.Language)\PowerToys.ImageResizer.resources.dll" />
|
|
||||||
</Component>
|
|
||||||
<Component Id="ColorPicker_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)03">
|
<Component Id="ColorPicker_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)03">
|
||||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
<RegistryValue Type="string" Name="ColorPicker_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
<RegistryValue Type="string" Name="ColorPicker_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
||||||
@@ -459,7 +453,6 @@
|
|||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall" />
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall" />
|
||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall" />
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall" />
|
||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall" />
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall" />
|
||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall"/>
|
|
||||||
<?undef IdSafeLanguage?>
|
<?undef IdSafeLanguage?>
|
||||||
<?endforeach?>
|
<?endforeach?>
|
||||||
</Component>
|
</Component>
|
||||||
|
|||||||
@@ -131,7 +131,25 @@ if ($platform -ceq "arm64") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#BaseApplications
|
#BaseApplications
|
||||||
|
# WORKAROUND: Exclude ImageResizer files that leak into the root output directory.
|
||||||
|
# ImageResizerCLI (Exe, SelfContained) has a ProjectReference to ImageResizerUI (WinExe, SelfContained).
|
||||||
|
# MSBuild copies the referenced WinExe's apphost (.exe, .deps.json, .runtimeconfig.json) to the root
|
||||||
|
# output directory as a side effect. These files are incomplete (missing the managed .dll) and should
|
||||||
|
# not be included in the installer. The complete ImageResizer files are in WinUI3Apps/ and are handled
|
||||||
|
# by WinUI3ApplicationsFiles. TODO: Refactor ImageResizer to use a shared Library project instead.
|
||||||
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
|
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
|
||||||
|
|
||||||
|
# Remove leaked ImageResizer artifacts from BaseApplications
|
||||||
|
$baseAppWxsPath = "$PSScriptRoot\BaseApplications.wxs"
|
||||||
|
$baseAppWxs = Get-Content $baseAppWxsPath -Raw
|
||||||
|
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.exe;?', ''
|
||||||
|
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.deps\.json;?', ''
|
||||||
|
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json;?', ''
|
||||||
|
# Clean up trailing/double semicolons left after removal
|
||||||
|
$baseAppWxs = $baseAppWxs -replace ';;+', ';'
|
||||||
|
$baseAppWxs = $baseAppWxs -replace '=;', '='
|
||||||
|
$baseAppWxs = $baseAppWxs -replace ';"', '"'
|
||||||
|
Set-Content -Path $baseAppWxsPath -Value $baseAppWxs
|
||||||
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
|
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
|
||||||
|
|
||||||
#WinUI3Applications
|
#WinUI3Applications
|
||||||
|
|||||||
@@ -20,9 +20,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
|
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
|
|
||||||
<ItemGroup>
|
|
||||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -50,10 +50,10 @@
|
|||||||
<EnableUAC>false</EnableUAC>
|
<EnableUAC>false</EnableUAC>
|
||||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||||
</Link>
|
</Link>
|
||||||
<PreBuildEvent>
|
<PostBuildEvent>
|
||||||
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
|
<Command>if exist "$(OutDir)ImageResizerContextMenuPackage.msix" del "$(OutDir)ImageResizerContextMenuPackage.msix" /q
|
||||||
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
|
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)ImageResizerContextMenuPackage.msix" /nv</Command>
|
||||||
</PreBuildEvent>
|
</PostBuildEvent>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
@@ -73,10 +73,10 @@ MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Comm
|
|||||||
<EnableUAC>false</EnableUAC>
|
<EnableUAC>false</EnableUAC>
|
||||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||||
</Link>
|
</Link>
|
||||||
<PreBuildEvent>
|
<PostBuildEvent>
|
||||||
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
|
<Command>if exist "$(OutDir)ImageResizerContextMenuPackage.msix" del "$(OutDir)ImageResizerContextMenuPackage.msix" /q
|
||||||
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
|
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)ImageResizerContextMenuPackage.msix" /nv</Command>
|
||||||
</PreBuildEvent>
|
</PostBuildEvent>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="framework.h" />
|
<ClInclude Include="framework.h" />
|
||||||
|
|||||||
@@ -25,24 +25,3 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001:XmlCommentAnalysisDisabled", Justification = "Not enabled as we don't want or need XML documentation.")]
|
[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001:XmlCommentAnalysisDisabled", Justification = "Not enabled as we don't want or need XML documentation.")]
|
||||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:DocumentationTextMustEndWithAPeriod", Justification = "Not enabled as we don't want or need XML documentation.")]
|
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:DocumentationTextMustEndWithAPeriod", Justification = "Not enabled as we don't want or need XML documentation.")]
|
||||||
|
|
||||||
[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Scope = "member", Target = "Microsoft.Templates.Core.Locations.TemplatesSynchronization.#SyncStatusChanged", Justification = "Using an Action<object, SyncStatusEventArgs> does not allow the required notation")]
|
|
||||||
|
|
||||||
// Non general suppressions
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "This is part of the markdown processing", MessageId = "System.Windows.Documents.Run.#ctor(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Controls.Markdown.#ImageInlineEvaluator(System.Text.RegularExpressions.Match)")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.ITemplateInfoExtensions.#GetQueryableProperties(Microsoft.TemplateEngine.Abstractions.ITemplateInfo)")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.Composition.CompositionQuery.#Match(System.Collections.Generic.IEnumerable`1<Microsoft.Templates.Core.Composition.QueryNode>,Microsoft.Templates.Core.Composition.QueryablePropertyDictionary)")]
|
|
||||||
[assembly: SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "Resource DictionaryWriter does not implement flush async", Scope = "member", Target = "~M:Microsoft.Templates.Core.PostActions.Catalog.Merge.MergeResourceDictionaryPostAction.ExecuteInternalAsync~System.Threading.Tasks.Task")]
|
|
||||||
|
|
||||||
// Threading suppressions
|
|
||||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.Controls.Notification.OnClose")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel.OnDelete")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.WizardNavigation.GoBack")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.WizardNavigation.GoForward")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel.OnDelete(Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel)")]
|
|
||||||
|
|
||||||
// Localization suppressions
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#CreateJunction(System.String,System.String,System.Boolean)", Justification = "Only used for local generation")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#DeleteJunction(System.String)", Justification = "Only used for local generation")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#InternalGetTarget(Microsoft.Win32.SafeHandles.SafeFileHandle)", Justification = "Only used for local generation")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#OpenReparsePoint(System.String,Microsoft.Templates.Core.Locations.JunctionNativeMethods+EFileAccess)", Justification = "Only used for local generation")]
|
|
||||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Windows.Documents.InlineCollection.Add(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Extensions.TextBlockExtensions.#OnSequentialFlowStepChanged(System.Windows.DependencyObject,System.Windows.DependencyPropertyChangedEventArgs)", Justification = "No text here")]
|
|
||||||
|
|||||||
@@ -82,4 +82,4 @@ IDR_CONTEXTMENUHANDLER REGISTRY "ContextMenuHandler.rgs"
|
|||||||
|
|
||||||
// Icon with lowest ID value placed first to ensure application icon
|
// Icon with lowest ID value placed first to ensure application icon
|
||||||
// remains consistent on all systems.
|
// remains consistent on all systems.
|
||||||
IDI_RESIZE_PICTURES ICON "..\ui\Resources\ImageResizer.ico"
|
IDI_RESIZE_PICTURES ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
<AssemblyName>ImageResizer.Test</AssemblyName>
|
<AssemblyName>ImageResizer.Test</AssemblyName>
|
||||||
|
|
||||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\</OutputPath>
|
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\</OutputPath>
|
||||||
|
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
|
||||||
{
|
|
||||||
[TestClass]
|
|
||||||
public class CustomSizeTests
|
|
||||||
{
|
|
||||||
[TestMethod]
|
|
||||||
public void NameWorks()
|
|
||||||
{
|
|
||||||
var size = new CustomSize
|
|
||||||
{
|
|
||||||
Name = "Ignored",
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.AreEqual(Resources.Input_Custom, size.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -50,29 +51,14 @@ namespace ImageResizer.Models
|
|||||||
Assert.AreEqual("OutputDir", result.DestinationDirectory);
|
Assert.AreEqual("OutputDir", result.DestinationDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*[Fact]
|
|
||||||
public void Process_executes_in_parallel()
|
|
||||||
{
|
|
||||||
var batch = CreateBatch(_ => Thread.Sleep(50));
|
|
||||||
batch.Files.AddRange(
|
|
||||||
Enumerable.Range(0, Environment.ProcessorCount)
|
|
||||||
.Select(i => "Image" + i + ".jpg"));
|
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
batch.Process(CancellationToken.None, (_, __) => { });
|
|
||||||
stopwatch.Stop();
|
|
||||||
|
|
||||||
Assert.InRange(stopwatch.ElapsedMilliseconds, 50, 99);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ProcessAggregatesErrors()
|
public async Task ProcessAggregatesErrors()
|
||||||
{
|
{
|
||||||
var batch = CreateBatch(file => throw new InvalidOperationException("Error: " + file));
|
var batch = CreateBatch(file => throw new InvalidOperationException("Error: " + file));
|
||||||
batch.Files.Add("Image1.jpg");
|
batch.Files.Add("Image1.jpg");
|
||||||
batch.Files.Add("Image2.jpg");
|
batch.Files.Add("Image2.jpg");
|
||||||
|
|
||||||
var errors = batch.Process((_, __) => { }, CancellationToken.None).ToList();
|
var errors = (await batch.ProcessAsync((_, __) => { }, CancellationToken.None)).ToList();
|
||||||
|
|
||||||
Assert.AreEqual(2, errors.Count);
|
Assert.AreEqual(2, errors.Count);
|
||||||
|
|
||||||
@@ -91,14 +77,14 @@ namespace ImageResizer.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ProcessReportsProgress()
|
public async Task ProcessReportsProgress()
|
||||||
{
|
{
|
||||||
var batch = CreateBatch(_ => { });
|
var batch = CreateBatch(_ => { });
|
||||||
batch.Files.Add("Image1.jpg");
|
batch.Files.Add("Image1.jpg");
|
||||||
batch.Files.Add("Image2.jpg");
|
batch.Files.Add("Image2.jpg");
|
||||||
var calls = new ConcurrentBag<(int I, double Count)>();
|
var calls = new ConcurrentBag<(int I, double Count)>();
|
||||||
|
|
||||||
batch.Process(
|
await batch.ProcessAsync(
|
||||||
(i, count) => calls.Add((i, count)),
|
(i, count) => calls.Add((i, count)),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
@@ -109,8 +95,12 @@ namespace ImageResizer.Models
|
|||||||
{
|
{
|
||||||
var mock = new Mock<ResizeBatch> { CallBase = true };
|
var mock = new Mock<ResizeBatch> { CallBase = true };
|
||||||
mock.Protected()
|
mock.Protected()
|
||||||
.Setup("Execute", ItExpr.IsAny<string>(), ItExpr.IsAny<Settings>())
|
.Setup<Task>("ExecuteAsync", ItExpr.IsAny<string>(), ItExpr.IsAny<Settings>())
|
||||||
.Callback((string file, Settings settings) => executeAction(file));
|
.Returns((string file, Settings settings) =>
|
||||||
|
{
|
||||||
|
executeAction(file);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
return mock.Object;
|
return mock.Object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Media;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
|
|
||||||
using ImageResizer.Extensions;
|
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
using ImageResizer.Test;
|
using ImageResizer.Test;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
@@ -20,45 +18,61 @@ namespace ImageResizer.Models
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class ResizeOperationTests : IDisposable
|
public class ResizeOperationTests : IDisposable
|
||||||
{
|
{
|
||||||
|
// Known legacy container format GUID for PNG, used as FallbackEncoder value in settings JSON
|
||||||
|
private static readonly Guid PngContainerFormatGuid = new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf");
|
||||||
|
|
||||||
|
private static readonly string[] CommentPropertyQuery = new[] { "System.Comment" };
|
||||||
|
private static readonly string[] DateTakenPropertyQuery = new[] { "System.Photo.DateTaken" };
|
||||||
|
private static readonly string[] CameraModelPropertyQuery = new[] { "System.Photo.CameraModel" };
|
||||||
|
|
||||||
private readonly TestDirectory _directory = new TestDirectory();
|
private readonly TestDirectory _directory = new TestDirectory();
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteCopiesFrameMetadata()
|
public async Task ExecuteCopiesFrameMetadata()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation("Test.jpg", _directory, Settings());
|
var operation = new ResizeOperation("Test.jpg", _directory, Settings());
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment));
|
async decoder =>
|
||||||
|
{
|
||||||
|
var props = await decoder.BitmapProperties.GetPropertiesAsync(CommentPropertyQuery);
|
||||||
|
Assert.IsTrue(props.ContainsKey("System.Comment"), "Comment metadata should be preserved during transcode");
|
||||||
|
Assert.AreEqual("Test", (string)props["System.Comment"].Value, "Comment value should be preserved");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
|
public async Task ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation("TestMetadataIssue2447.jpg", _directory, Settings());
|
var operation = new ResizeOperation("TestMetadataIssue2447.jpg", _directory, Settings());
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).CameraModel));
|
async decoder =>
|
||||||
|
{
|
||||||
|
var props = await decoder.BitmapProperties.GetPropertiesAsync(CameraModelPropertyQuery);
|
||||||
|
Assert.IsTrue(props.ContainsKey("System.Photo.CameraModel"), "Camera model metadata should be preserved");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteKeepsDateModified()
|
public async Task ExecuteKeepsDateModified()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation("Test.png", _directory, Settings(s => s.KeepDateModified = true));
|
var operation = new ResizeOperation("Test.png", _directory, Settings(s => s.KeepDateModified = true));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
Assert.AreEqual(File.GetLastWriteTimeUtc("Test.png"), File.GetLastWriteTimeUtc(_directory.File()));
|
Assert.AreEqual(File.GetLastWriteTimeUtc("Test.png"), File.GetLastWriteTimeUtc(_directory.File()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteKeepsDateModifiedWhenReplacingOriginals()
|
public async Task ExecuteKeepsDateModifiedWhenReplacingOriginals()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_directory, "Test.png");
|
var path = Path.Combine(_directory, "Test.png");
|
||||||
File.Copy("Test.png", path);
|
File.Copy("Test.png", path);
|
||||||
@@ -75,55 +89,59 @@ namespace ImageResizer.Models
|
|||||||
s.Replace = true;
|
s.Replace = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
Assert.AreEqual(originalDateModified, File.GetLastWriteTimeUtc(_directory.File()));
|
Assert.AreEqual(originalDateModified, File.GetLastWriteTimeUtc(_directory.File()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteReplacesOriginals()
|
public async Task ExecuteReplacesOriginals()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_directory, "Test.png");
|
var path = Path.Combine(_directory, "Test.png");
|
||||||
File.Copy("Test.png", path);
|
File.Copy("Test.png", path);
|
||||||
|
|
||||||
var operation = new ResizeOperation(path, null, Settings(s => s.Replace = true));
|
var operation = new ResizeOperation(path, null, Settings(s => s.Replace = true));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(_directory.File(), image => Assert.AreEqual(96, image.Frames[0].PixelWidth));
|
await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual(96u, decoder.PixelWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteTransformsEachFrame()
|
public async Task ExecuteTransformsEachFrame()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation("Test.gif", _directory, Settings());
|
var operation = new ResizeOperation("Test.gif", _directory, Settings());
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
async decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(2, image.Frames.Count);
|
Assert.AreEqual(2u, decoder.FrameCount);
|
||||||
AssertEx.All(image.Frames, frame => Assert.AreEqual(96, frame.PixelWidth));
|
for (uint i = 0; i < decoder.FrameCount; i++)
|
||||||
|
{
|
||||||
|
var frame = await decoder.GetFrameAsync(i);
|
||||||
|
Assert.AreEqual(96u, frame.PixelWidth);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteUsesFallbackEncoder()
|
public async Task ExecuteUsesFallbackEncoder()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.ico",
|
"Test.ico",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(s => s.FallbackEncoder = new PngBitmapEncoder().CodecInfo.ContainerFormat));
|
Settings(s => s.FallbackEncoder = PngContainerFormatGuid));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test).png");
|
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test).png");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformIgnoresOrientationWhenLandscapeToPortrait()
|
public async Task TransformIgnoresOrientationWhenLandscapeToPortrait()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -136,19 +154,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 192;
|
x.SelectedSize.Height = 192;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(192, image.Frames[0].PixelWidth);
|
Assert.AreEqual(192u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformIgnoresOrientationWhenPortraitToLandscape()
|
public async Task TransformIgnoresOrientationWhenPortraitToLandscape()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"TestPortrait.png",
|
"TestPortrait.png",
|
||||||
@@ -161,19 +179,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 96;
|
x.SelectedSize.Height = 96;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(192, image.Frames[0].PixelHeight);
|
Assert.AreEqual(192u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformIgnoresIgnoreOrientationWhenAuto()
|
public async Task TransformIgnoresIgnoreOrientationWhenAuto()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -186,19 +204,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 0;
|
x.SelectedSize.Height = 0;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(48, image.Frames[0].PixelHeight);
|
Assert.AreEqual(48u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformIgnoresIgnoreOrientationWhenPercent()
|
public async Task TransformIgnoresIgnoreOrientationWhenPercent()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -213,19 +231,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Fit = ResizeFit.Stretch;
|
x.SelectedSize.Fit = ResizeFit.Stretch;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(192, image.Frames[0].PixelHeight);
|
Assert.AreEqual(192u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsShrinkOnly()
|
public async Task TransformHonorsShrinkOnly()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -238,19 +256,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 288;
|
x.SelectedSize.Height = 288;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(192, image.Frames[0].PixelWidth);
|
Assert.AreEqual(192u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformIgnoresShrinkOnlyWhenPercent()
|
public async Task TransformIgnoresShrinkOnlyWhenPercent()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -263,19 +281,19 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Unit = ResizeUnit.Percent;
|
x.SelectedSize.Unit = ResizeUnit.Percent;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(256, image.Frames[0].PixelWidth);
|
Assert.AreEqual(256u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(128, image.Frames[0].PixelHeight);
|
Assert.AreEqual(128u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsShrinkOnlyWhenAutoHeight()
|
public async Task TransformHonorsShrinkOnlyWhenAutoHeight()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -288,15 +306,15 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 0;
|
x.SelectedSize.Height = 0;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.AreEqual(192, image.Frames[0].PixelWidth));
|
decoder => Assert.AreEqual(192u, decoder.PixelWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsShrinkOnlyWhenAutoWidth()
|
public async Task TransformHonorsShrinkOnlyWhenAutoWidth()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -309,15 +327,15 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 288;
|
x.SelectedSize.Height = 288;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.AreEqual(96, image.Frames[0].PixelHeight));
|
decoder => Assert.AreEqual(96u, decoder.PixelHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsUnit()
|
public async Task TransformHonorsUnit()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -330,82 +348,79 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Unit = ResizeUnit.Inch;
|
x.SelectedSize.Unit = ResizeUnit.Inch;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(_directory.File(), image => Assert.AreEqual(Math.Ceiling(image.Frames[0].DpiX), image.Frames[0].PixelWidth));
|
await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual((uint)Math.Ceiling(decoder.DpiX), decoder.PixelWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFitWhenFit()
|
public async Task TransformHonorsFitWhenFit()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(x => x.SelectedSize.Fit = ResizeFit.Fit));
|
Settings(x => x.SelectedSize.Fit = ResizeFit.Fit));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(48, image.Frames[0].PixelHeight);
|
Assert.AreEqual(48u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFitWhenFill()
|
public async Task TransformHonorsFitWhenFill()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(x => x.SelectedSize.Fit = ResizeFit.Fill));
|
Settings(x => x.SelectedSize.Fit = ResizeFit.Fill));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
async decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(Colors.White, image.Frames[0].GetFirstPixel());
|
var pixel = await decoder.GetFirstPixelAsync();
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual((byte)255, pixel.R, "First pixel R should be 255 (white)");
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual((byte)255, pixel.G, "First pixel G should be 255 (white)");
|
||||||
|
Assert.AreEqual((byte)255, pixel.B, "First pixel B should be 255 (white)");
|
||||||
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFitWhenStretch()
|
public async Task TransformHonorsFitWhenStretch()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(x => x.SelectedSize.Fit = ResizeFit.Stretch));
|
Settings(x => x.SelectedSize.Fit = ResizeFit.Stretch));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
async decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(Colors.Black, image.Frames[0].GetFirstPixel());
|
var pixel = await decoder.GetFirstPixelAsync();
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual((byte)0, pixel.R, "First pixel R should be 0 (black)");
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual((byte)0, pixel.G, "First pixel G should be 0 (black)");
|
||||||
|
Assert.AreEqual((byte)0, pixel.B, "First pixel B should be 0 (black)");
|
||||||
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFillWithShrinkOnlyWhenCropRequired()
|
public async Task TransformHonorsFillWithShrinkOnlyWhenCropRequired()
|
||||||
{
|
{
|
||||||
// Testing original 96x96 pixel Test.jpg cropped to 48x96 (Fill mode).
|
|
||||||
//
|
|
||||||
// ScaleX = 48/96 = 0.5
|
|
||||||
// ScaleY = 96/96 = 1.0
|
|
||||||
// Fill mode takes the max of these = 1.0.
|
|
||||||
//
|
|
||||||
// Previously, the transform logic saw the scale of 1.0 and returned the
|
|
||||||
// original dimensions. The corrected logic recognizes that a crop is
|
|
||||||
// required on one dimension and proceeds with the operation.
|
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.jpg",
|
"Test.jpg",
|
||||||
_directory,
|
_directory,
|
||||||
@@ -417,22 +432,20 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 96;
|
x.SelectedSize.Height = 96;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(48, image.Frames[0].PixelWidth);
|
Assert.AreEqual(48u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
|
public async Task TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
|
||||||
{
|
{
|
||||||
// Confirm that attempting to upscale the original image will return the
|
|
||||||
// original dimensions when Shrink Only is enabled.
|
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.jpg",
|
"Test.jpg",
|
||||||
_directory,
|
_directory,
|
||||||
@@ -444,21 +457,20 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 192;
|
x.SelectedSize.Height = 192;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
|
public async Task TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
|
||||||
{
|
{
|
||||||
// With a scale of 1.0 on both axes, the original should be returned.
|
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.jpg",
|
"Test.jpg",
|
||||||
_directory,
|
_directory,
|
||||||
@@ -470,70 +482,70 @@ namespace ImageResizer.Models
|
|||||||
x.SelectedSize.Height = 96;
|
x.SelectedSize.Height = 96;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image =>
|
decoder =>
|
||||||
{
|
{
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||||
Assert.AreEqual(96, image.Frames[0].PixelHeight);
|
Assert.AreEqual(96u, decoder.PixelHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void GetDestinationPathUniquifiesOutputFilename()
|
public async Task GetDestinationPathUniquifiesOutputFilename()
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
|
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
|
||||||
|
|
||||||
var operation = new ResizeOperation("Test.png", _directory, Settings());
|
var operation = new ResizeOperation("Test.png", _directory, Settings());
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (1).png");
|
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (1).png");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void GetDestinationPathUniquifiesOutputFilenameAgain()
|
public async Task GetDestinationPathUniquifiesOutputFilenameAgain()
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
|
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
|
||||||
File.WriteAllBytes(Path.Combine(_directory, "Test (Test) (1).png"), Array.Empty<byte>());
|
File.WriteAllBytes(Path.Combine(_directory, "Test (Test) (1).png"), Array.Empty<byte>());
|
||||||
|
|
||||||
var operation = new ResizeOperation("Test.png", _directory, Settings());
|
var operation = new ResizeOperation("Test.png", _directory, Settings());
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (2).png");
|
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (2).png");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void GetDestinationPathUsesFileNameFormat()
|
public async Task GetDestinationPathUsesFileNameFormat()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(s => s.FileName = "%1_%2_%3_%4_%5_%6"));
|
Settings(s => s.FileName = "%1_%2_%3_%4_%5_%6"));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test_Test_96_96_96_48.png");
|
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test_Test_96_96_96_48.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ExecuteHandlesDirectoriesInFileNameFormat()
|
public async Task ExecuteHandlesDirectoriesInFileNameFormat()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
_directory,
|
_directory,
|
||||||
Settings(s => s.FileName = @"Directory\%1 (%2)"));
|
Settings(s => s.FileName = @"Directory\%1 (%2)"));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test (Test).png"));
|
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test (Test).png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void StripMetadata()
|
public async Task StripMetadata()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"TestMetadataIssue1928.jpg",
|
"TestMetadataIssue1928.jpg",
|
||||||
@@ -544,18 +556,26 @@ namespace ImageResizer.Models
|
|||||||
x.RemoveMetadata = true;
|
x.RemoveMetadata = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
|
async decoder =>
|
||||||
AssertEx.Image(
|
{
|
||||||
_directory.File(),
|
try
|
||||||
image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
|
{
|
||||||
|
var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
|
||||||
|
Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should be stripped");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// If GetPropertiesAsync throws, metadata is not present — which is expected
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void StripMetadataWhenNoMetadataPresent()
|
public async Task StripMetadataWhenNoMetadataPresent()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"TestMetadataIssue1928_NoMetadata.jpg",
|
"TestMetadataIssue1928_NoMetadata.jpg",
|
||||||
@@ -566,18 +586,26 @@ namespace ImageResizer.Models
|
|||||||
x.RemoveMetadata = true;
|
x.RemoveMetadata = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
AssertEx.Image(
|
await AssertEx.ImageAsync(
|
||||||
_directory.File(),
|
_directory.File(),
|
||||||
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
|
async decoder =>
|
||||||
AssertEx.Image(
|
{
|
||||||
_directory.File(),
|
try
|
||||||
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
|
{
|
||||||
|
var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
|
||||||
|
Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should not exist");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Expected: no metadata block at all
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void VerifyFileNameIsSanitized()
|
public async Task VerifyFileNameIsSanitized()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -589,13 +617,13 @@ namespace ImageResizer.Models
|
|||||||
s.SelectedSize.Name = "Test\\/";
|
s.SelectedSize.Name = "Test\\/";
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test_______(Test__).png"));
|
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test_______(Test__).png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void VerifyNotRecommendedNameIsChanged()
|
public async Task VerifyNotRecommendedNameIsChanged()
|
||||||
{
|
{
|
||||||
var operation = new ResizeOperation(
|
var operation = new ResizeOperation(
|
||||||
"Test.png",
|
"Test.png",
|
||||||
@@ -606,7 +634,7 @@ namespace ImageResizer.Models
|
|||||||
s.FileName = @"nul";
|
s.FileName = @"nul";
|
||||||
}));
|
}));
|
||||||
|
|
||||||
operation.Execute();
|
await operation.ExecuteAsync();
|
||||||
|
|
||||||
Assert.IsTrue(File.Exists(_directory + @"\nul_.png"));
|
Assert.IsTrue(File.Exists(_directory + @"\nul_.png"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Test;
|
using ImageResizer.Test;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
@@ -17,6 +17,12 @@ namespace ImageResizer.Models
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class ResizeSizeTests
|
public class ResizeSizeTests
|
||||||
{
|
{
|
||||||
|
[ClassInitialize]
|
||||||
|
public static void ClassInit(TestContext context)
|
||||||
|
{
|
||||||
|
ResourceLoaderInstance.GetString = key => key;
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void NameWorks()
|
public void NameWorks()
|
||||||
{
|
{
|
||||||
@@ -34,22 +40,11 @@ namespace ImageResizer.Models
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void NameReplacesTokens()
|
public void NameReplacesTokens()
|
||||||
{
|
{
|
||||||
var args = new List<(string, string)>
|
var size = new ResizeSize();
|
||||||
{
|
|
||||||
("$small$", Resources.Small),
|
|
||||||
("$medium$", Resources.Medium),
|
|
||||||
("$large$", Resources.Large),
|
|
||||||
("$phone$", Resources.Phone),
|
|
||||||
};
|
|
||||||
foreach (var (name, expected) in args)
|
|
||||||
{
|
|
||||||
var size = new ResizeSize
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.AreEqual(expected, size.Name);
|
size.Name = "$small$";
|
||||||
}
|
|
||||||
|
Assert.AreEqual("Small", size.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -57,13 +52,15 @@ namespace ImageResizer.Models
|
|||||||
{
|
{
|
||||||
var size = new ResizeSize();
|
var size = new ResizeSize();
|
||||||
|
|
||||||
var e = AssertEx.Raises<PropertyChangedEventArgs>(
|
var events = AssertEx.RaisesAll<PropertyChangedEventArgs>(
|
||||||
h => size.PropertyChanged += h,
|
h => size.PropertyChanged += h,
|
||||||
h => size.PropertyChanged -= h,
|
h => size.PropertyChanged -= h,
|
||||||
() => size.Fit = ResizeFit.Stretch);
|
() => size.Fit = ResizeFit.Stretch);
|
||||||
|
|
||||||
Assert.AreEqual(ResizeFit.Stretch, size.Fit);
|
Assert.AreEqual(ResizeFit.Stretch, size.Fit);
|
||||||
Assert.AreEqual(nameof(ResizeSize.Fit), e.Arguments.PropertyName);
|
Assert.IsTrue(
|
||||||
|
events.Any(e => e.Arguments.PropertyName == nameof(ResizeSize.Fit)),
|
||||||
|
"Expected PropertyChanged for Fit");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -135,13 +132,15 @@ namespace ImageResizer.Models
|
|||||||
{
|
{
|
||||||
var size = new ResizeSize();
|
var size = new ResizeSize();
|
||||||
|
|
||||||
var e = AssertEx.Raises<PropertyChangedEventArgs>(
|
var events = AssertEx.RaisesAll<PropertyChangedEventArgs>(
|
||||||
h => size.PropertyChanged += h,
|
h => size.PropertyChanged += h,
|
||||||
h => size.PropertyChanged -= h,
|
h => size.PropertyChanged -= h,
|
||||||
() => size.Unit = ResizeUnit.Inch);
|
() => size.Unit = ResizeUnit.Inch);
|
||||||
|
|
||||||
Assert.AreEqual(ResizeUnit.Inch, size.Unit);
|
Assert.AreEqual(ResizeUnit.Inch, size.Unit);
|
||||||
Assert.AreEqual(nameof(ResizeSize.Unit), e.Arguments.PropertyName);
|
Assert.IsTrue(
|
||||||
|
events.Any(e => e.Arguments.PropertyName == nameof(ResizeSize.Unit)),
|
||||||
|
"Expected PropertyChanged for Unit");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Test;
|
using ImageResizer.Test;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
@@ -25,10 +26,6 @@ namespace ImageResizer.Properties
|
|||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
|
|
||||||
|
|
||||||
private static App _imageResizerApp;
|
|
||||||
|
|
||||||
public SettingsTests()
|
public SettingsTests()
|
||||||
{
|
{
|
||||||
// Change settings.json path to a temp file
|
// Change settings.json path to a temp file
|
||||||
@@ -38,8 +35,7 @@ namespace ImageResizer.Properties
|
|||||||
[ClassInitialize]
|
[ClassInitialize]
|
||||||
public static void ClassInitialize(TestContext context)
|
public static void ClassInitialize(TestContext context)
|
||||||
{
|
{
|
||||||
// new App() needs to be created since Settings.Reload() uses App.Current to update properties on the UI thread. App() can be created only once otherwise it results in System.InvalidOperationException : Cannot create more than one System.Windows.Application instance in the same AppDomain.
|
ResourceLoaderInstance.GetString = key => key;
|
||||||
_imageResizerApp = new App();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -193,9 +189,11 @@ namespace ImageResizer.Properties
|
|||||||
|
|
||||||
var result = ((IDataErrorInfo)settings)["JpegQualityLevel"];
|
var result = ((IDataErrorInfo)settings)["JpegQualityLevel"];
|
||||||
|
|
||||||
// Using InvariantCulture since this is used internally
|
// With test ResourceLoaderInstance, GetString returns the key itself ("ValueMustBeBetween")
|
||||||
|
// which becomes the CompositeFormat. Format it the same way the production code does.
|
||||||
|
var expectedFormat = CompositeFormat.Parse(ResourceLoaderInstance.GetString("ValueMustBeBetween"));
|
||||||
Assert.AreEqual(
|
Assert.AreEqual(
|
||||||
string.Format(CultureInfo.InvariantCulture, ValueMustBeBetween, 1, 100),
|
string.Format(CultureInfo.InvariantCulture, expectedFormat, 1, 100),
|
||||||
result);
|
result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +273,11 @@ namespace ImageResizer.Properties
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var settings = new Settings();
|
var settings = new Settings();
|
||||||
settings.Save(); // To create the settings file
|
settings.SelectedSizeIndex = 2;
|
||||||
|
settings.Save();
|
||||||
|
|
||||||
|
// Reset to default so Reload will trigger a real change
|
||||||
|
settings.SelectedSizeIndex = 0;
|
||||||
|
|
||||||
var shrinkOnlyChanged = false;
|
var shrinkOnlyChanged = false;
|
||||||
var replaceChanged = false;
|
var replaceChanged = false;
|
||||||
@@ -385,8 +387,7 @@ namespace ImageResizer.Properties
|
|||||||
[ClassCleanup]
|
[ClassCleanup]
|
||||||
public static void ClassCleanup()
|
public static void ClassCleanup()
|
||||||
{
|
{
|
||||||
_imageResizerApp.Dispose();
|
// No App instance to dispose in WinUI3 test environment
|
||||||
_imageResizerApp = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
using Windows.Graphics.Imaging;
|
||||||
|
|
||||||
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "File created under PowerToys.")]
|
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "File created under PowerToys.")]
|
||||||
|
|
||||||
namespace ImageResizer.Test
|
namespace ImageResizer.Test
|
||||||
@@ -29,17 +32,20 @@ namespace ImageResizer.Test
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Image(string path, Action<BitmapDecoder> action)
|
public static async Task ImageAsync(string path, Action<BitmapDecoder> action)
|
||||||
{
|
{
|
||||||
using (var stream = _fileSystem.File.OpenRead(path))
|
using var stream = _fileSystem.File.OpenRead(path);
|
||||||
{
|
var winrtStream = stream.AsRandomAccessStream();
|
||||||
var image = BitmapDecoder.Create(
|
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||||
stream,
|
action(decoder);
|
||||||
BitmapCreateOptions.PreservePixelFormat,
|
}
|
||||||
BitmapCacheOption.None);
|
|
||||||
|
|
||||||
action(image);
|
public static async Task ImageAsync(string path, Func<BitmapDecoder, Task> action)
|
||||||
}
|
{
|
||||||
|
using var stream = _fileSystem.File.OpenRead(path);
|
||||||
|
var winrtStream = stream.AsRandomAccessStream();
|
||||||
|
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||||
|
await action(decoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RaisedEvent<NotifyCollectionChangedEventArgs> Raises<T>(
|
public static RaisedEvent<NotifyCollectionChangedEventArgs> Raises<T>(
|
||||||
@@ -78,6 +84,24 @@ namespace ImageResizer.Test
|
|||||||
return raisedEvent;
|
return raisedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IList<RaisedEvent<PropertyChangedEventArgs>> RaisesAll<T>(
|
||||||
|
Action<PropertyChangedEventHandler> attach,
|
||||||
|
Action<PropertyChangedEventHandler> detach,
|
||||||
|
Action testCode)
|
||||||
|
where T : PropertyChangedEventArgs
|
||||||
|
{
|
||||||
|
var events = new List<RaisedEvent<PropertyChangedEventArgs>>();
|
||||||
|
PropertyChangedEventHandler handler = (sender, e)
|
||||||
|
=> events.Add(new RaisedEvent<PropertyChangedEventArgs>(sender, e));
|
||||||
|
attach(handler);
|
||||||
|
testCode();
|
||||||
|
detach(handler);
|
||||||
|
|
||||||
|
Assert.IsTrue(events.Count > 0, "Expected at least one PropertyChanged event.");
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class RaisedEvent<TArgs>
|
public sealed class RaisedEvent<TArgs>
|
||||||
{
|
{
|
||||||
public RaisedEvent(object sender, TArgs args)
|
public RaisedEvent(object sender, TArgs args)
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073
|
||||||
|
|
||||||
using System.Windows;
|
using System;
|
||||||
using System.Windows.Media;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
|
using Windows.Storage.Streams;
|
||||||
|
|
||||||
namespace ImageResizer.Test
|
namespace ImageResizer.Test
|
||||||
{
|
{
|
||||||
internal static class BitmapSourceExtensions
|
internal static class BitmapSourceExtensions
|
||||||
{
|
{
|
||||||
public static Color GetFirstPixel(this BitmapSource source)
|
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(this BitmapDecoder decoder)
|
||||||
{
|
{
|
||||||
var pixel = new byte[4];
|
using var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
|
||||||
new FormatConvertedBitmap(
|
BitmapPixelFormat.Bgra8,
|
||||||
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
|
BitmapAlphaMode.Premultiplied);
|
||||||
PixelFormats.Bgra32,
|
|
||||||
destinationPalette: null,
|
|
||||||
alphaThreshold: 0)
|
|
||||||
.CopyPixels(pixel, 4, 0);
|
|
||||||
|
|
||||||
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
|
var buffer = new Windows.Storage.Streams.Buffer((uint)(softwareBitmap.PixelWidth * softwareBitmap.PixelHeight * 4));
|
||||||
|
softwareBitmap.CopyToBuffer(buffer);
|
||||||
|
|
||||||
|
using var reader = DataReader.FromBuffer(buffer);
|
||||||
|
byte b = reader.ReadByte();
|
||||||
|
byte g = reader.ReadByte();
|
||||||
|
byte r = reader.ReadByte();
|
||||||
|
byte a = reader.ReadByte();
|
||||||
|
|
||||||
|
return (r, g, b, a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public class TimeRemainingConverterTests
|
|
||||||
{
|
|
||||||
[DataTestMethod]
|
|
||||||
[DataRow("HourMinute", 1, 1, 0)]
|
|
||||||
[DataRow("HourMinutes", 1, 2, 0)]
|
|
||||||
[DataRow("HoursMinute", 2, 1, 0)]
|
|
||||||
[DataRow("HoursMinutes", 2, 2, 0)]
|
|
||||||
[DataRow("MinuteSecond", 0, 1, 1)]
|
|
||||||
[DataRow("MinuteSeconds", 0, 1, 2)]
|
|
||||||
[DataRow("MinutesSecond", 0, 2, 1)]
|
|
||||||
[DataRow("MinutesSeconds", 0, 2, 2)]
|
|
||||||
[DataRow("Second", 0, 0, 1)]
|
|
||||||
[DataRow("Seconds", 0, 0, 2)]
|
|
||||||
public void ConvertWorks(string resource, int hours, int minutes, int seconds)
|
|
||||||
{
|
|
||||||
var timeRemaining = new TimeSpan(hours, minutes, seconds);
|
|
||||||
var converter = new TimeRemainingConverter();
|
|
||||||
|
|
||||||
// Using InvariantCulture since these are internal
|
|
||||||
var result = converter.Convert(
|
|
||||||
timeRemaining,
|
|
||||||
targetType: null,
|
|
||||||
parameter: null,
|
|
||||||
CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
Assert.AreEqual(
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
Resources.ResourceManager.GetString("Progress_TimeRemaining_" + resource, CultureInfo.InvariantCulture),
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds),
|
|
||||||
result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<Application
|
|
||||||
x:Class="ImageResizer.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:m="clr-namespace:ImageResizer.Models"
|
|
||||||
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
|
||||||
xmlns:v="clr-namespace:ImageResizer.Views">
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ui:ThemesDictionary Theme="Dark" />
|
|
||||||
<ui:ControlsDictionary />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
|
|
||||||
<v:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
|
|
||||||
<v:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
|
|
||||||
<v:EnumValueConverter x:Key="EnumValueConverter" />
|
|
||||||
<v:AutoDoubleConverter x:Key="AutoDoubleConverter" />
|
|
||||||
<v:BoolValueConverter x:Key="BoolValueConverter" />
|
|
||||||
<v:VisibilityBoolConverter x:Key="VisibilityBoolConverter" />
|
|
||||||
<v:EnumToIntConverter x:Key="EnumToIntConverter" />
|
|
||||||
<v:AccessTextToTextConverter x:Key="AccessTextToTextConverter" />
|
|
||||||
<v:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
|
|
||||||
<v:ZeroToEmptyStringNumberFormatter x:Key="ZeroToEmptyStringNumberFormatter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Windows;
|
|
||||||
using ImageResizer.Models;
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
using ImageResizer.Utilities;
|
|
||||||
using ImageResizer.ViewModels;
|
|
||||||
using ImageResizer.Views;
|
|
||||||
using ManagedCommon;
|
|
||||||
|
|
||||||
namespace ImageResizer
|
|
||||||
{
|
|
||||||
public partial class App : Application, IDisposable
|
|
||||||
{
|
|
||||||
private const string LogSubFolder = "\\Image Resizer\\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();
|
|
||||||
if (!string.IsNullOrEmpty(appLanguage))
|
|
||||||
{
|
|
||||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (CultureNotFoundException ex)
|
|
||||||
{
|
|
||||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.InputEncoding = Encoding.Unicode;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
|
||||||
{
|
|
||||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
|
||||||
NativeMethods.SetProcessDPIAware();
|
|
||||||
|
|
||||||
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
|
|
||||||
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
|
|
||||||
#if true // Set to false to re-enable AI Super Resolution
|
|
||||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
|
||||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
|
||||||
|
|
||||||
// Skip AI detection mode as well
|
|
||||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
|
||||||
{
|
|
||||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
|
||||||
Environment.Exit(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Check for AI detection mode (called by Runner in background)
|
|
||||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
|
||||||
{
|
|
||||||
RunAiDetectionMode();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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)
|
|
||||||
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
|
|
||||||
mainWindow.Show();
|
|
||||||
|
|
||||||
// Temporary workaround for issue #1273
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
// AI feature disabled - always return NotSupported
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
// Dispose AI Super Resolution service
|
|
||||||
ResizeBatch.DisposeAiSuperResolutionService();
|
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
@@ -58,10 +59,10 @@ namespace ImageResizer.Cli
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RunSilentMode(cliOptions);
|
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int RunSilentMode(CliOptions cliOptions)
|
private async Task<int> RunSilentModeAsync(CliOptions cliOptions)
|
||||||
{
|
{
|
||||||
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
|
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
|
||||||
var settings = Settings.Default;
|
var settings = Settings.Default;
|
||||||
@@ -73,7 +74,7 @@ namespace ImageResizer.Cli
|
|||||||
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
|
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
|
||||||
int lastReportedMilestone = -1;
|
int lastReportedMilestone = -1;
|
||||||
|
|
||||||
var errors = batch.Process(
|
var errors = await batch.ProcessAsync(
|
||||||
(completed, total) =>
|
(completed, total) =>
|
||||||
{
|
{
|
||||||
var progress = (int)((completed / total) * 100);
|
var progress = (int)((completed / total) * 100);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// 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 ImageResizer.Helpers;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
|
namespace ImageResizer.Converters
|
||||||
|
{
|
||||||
|
public partial class AutoDoubleConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is double d && (d == 0 || double.IsNaN(d)))
|
||||||
|
{
|
||||||
|
return ResourceLoaderInstance.GetString("Auto");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/modules/imageresizer/ui/Converters/EnumToIntConverter.cs
Normal file
32
src/modules/imageresizer/ui/Converters/EnumToIntConverter.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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 Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
|
namespace ImageResizer.Converters
|
||||||
|
{
|
||||||
|
public partial class EnumToIntConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is Enum)
|
||||||
|
{
|
||||||
|
return System.Convert.ToInt32(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is int intValue && targetType.IsEnum)
|
||||||
|
{
|
||||||
|
return Enum.ToObject(targetType, intValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,24 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows.Data;
|
using ImageResizer.Helpers;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
namespace ImageResizer.Converters
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
{
|
||||||
[ValueConversion(typeof(Enum), typeof(string))]
|
public partial class EnumValueConverter : IValueConverter
|
||||||
public class EnumValueConverter : IValueConverter
|
|
||||||
{
|
{
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
{
|
{
|
||||||
var type = value?.GetType();
|
var type = value?.GetType();
|
||||||
if (!type.IsEnum)
|
if (type == null || !type.IsEnum)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -44,20 +42,18 @@ namespace ImageResizer.Views
|
|||||||
.Append(parameter);
|
.Append(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixes #16792 - Looks like culture defaults to en-US, so wrong resource is being fetched.
|
var targetValue = ResourceLoaderInstance.GetString(builder.ToString());
|
||||||
#pragma warning disable CA1304 // Specify CultureInfo
|
|
||||||
var targetValue = Resources.ResourceManager.GetString(builder.ToString());
|
|
||||||
#pragma warning restore CA1304 // Specify CultureInfo
|
|
||||||
|
|
||||||
if (toLower)
|
if (toLower && !string.IsNullOrEmpty(targetValue))
|
||||||
{
|
{
|
||||||
|
var culture = string.IsNullOrEmpty(language) ? CultureInfo.CurrentCulture : new CultureInfo(language);
|
||||||
targetValue = targetValue.ToLower(culture);
|
targetValue = targetValue.ToLower(culture);
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetValue;
|
return targetValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
=> value;
|
=> value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
namespace System.Windows.Media.Imaging
|
|
||||||
{
|
|
||||||
internal static class BitmapEncoderExtensions
|
|
||||||
{
|
|
||||||
public static bool CanEncode(this BitmapEncoder encoder)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var temp = encoder.CodecInfo;
|
|
||||||
}
|
|
||||||
catch (NotSupportedException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
// Copyright (c) Microsoft Corporation
|
|
||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
|
|
||||||
namespace ImageResizer.Extensions
|
|
||||||
{
|
|
||||||
internal static class BitmapMetadataExtension
|
|
||||||
{
|
|
||||||
public static void CopyMetadataPropertyTo(this BitmapMetadata source, BitmapMetadata target, string query)
|
|
||||||
{
|
|
||||||
if (source == null || target == null || string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = source.GetQuerySafe(query);
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
target.SetQuery(query, value);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
// InvalidOperationException is thrown if metadata object is in readonly state.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static object GetQuerySafe(this BitmapMetadata metadata, string query)
|
|
||||||
{
|
|
||||||
if (metadata == null || string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (metadata.ContainsQuery(query))
|
|
||||||
{
|
|
||||||
return metadata.GetQuery(query);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NotSupportedException)
|
|
||||||
{
|
|
||||||
// NotSupportedException is throw if the metadata entry is not preset on the target image (e.g. Orientation not set).
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void RemoveQuerySafe(this BitmapMetadata metadata, string query)
|
|
||||||
{
|
|
||||||
if (metadata == null || string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (metadata.ContainsQuery(query))
|
|
||||||
{
|
|
||||||
metadata.RemoveQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Exception while trying to remove metadata entry at position: {query}");
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetQuerySafe(this BitmapMetadata metadata, string query, object value)
|
|
||||||
{
|
|
||||||
if (metadata == null || string.IsNullOrWhiteSpace(query) || value == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
metadata.SetQuery(query, value);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Exception while trying to set metadata {value} at position: {query}");
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all metadata.
|
|
||||||
/// Iterates recursively through metadata and adds valid items to a list while skipping invalid data items.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Invalid data items are items which throw an exception when reading the data with metadata.GetQuery(...).
|
|
||||||
/// Sometimes Metadata collections are improper closed and cause an exception on IEnumerator.MoveNext(). In this case, we return all data items which were successfully collected so far.
|
|
||||||
/// </remarks>
|
|
||||||
/// <returns>
|
|
||||||
/// metadata path and metadata value of all successfully read data items.
|
|
||||||
/// </returns>
|
|
||||||
public static List<(string MetadataPath, object Value)> GetListOfMetadata(this BitmapMetadata metadata)
|
|
||||||
{
|
|
||||||
var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GetMetadataRecursively(metadata, string.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOfAllMetadata;
|
|
||||||
|
|
||||||
void GetMetadataRecursively(BitmapMetadata metadata, string query)
|
|
||||||
{
|
|
||||||
foreach (string relativeQuery in metadata)
|
|
||||||
{
|
|
||||||
string absolutePath = query + relativeQuery;
|
|
||||||
|
|
||||||
object metadataQueryReader = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
metadataQueryReader = GetQueryWithPreCheck(metadata, relativeQuery);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Removing corrupt metadata property {absolutePath}. Skipping metadata entry | {ex.Message}");
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataQueryReader != null)
|
|
||||||
{
|
|
||||||
listOfAllMetadata.Add((absolutePath, metadataQueryReader));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"No metadata found for query {absolutePath}. Skipping empty null entry because its invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataQueryReader is BitmapMetadata innerMetadata)
|
|
||||||
{
|
|
||||||
GetMetadataRecursively(innerMetadata, absolutePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object GetQueryWithPreCheck(BitmapMetadata metadata, string query)
|
|
||||||
{
|
|
||||||
if (metadata == null || string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.ContainsQuery(query))
|
|
||||||
{
|
|
||||||
return metadata.GetQuery(query);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints all metadata to debug console
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Intended for debug only!!!
|
|
||||||
/// </remarks>
|
|
||||||
public static void PrintsAllMetadataToDebugOutput(this BitmapMetadata metadata)
|
|
||||||
{
|
|
||||||
if (metadata == null)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Metadata was null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var listOfMetadata = metadata.GetListOfMetadataForDebug();
|
|
||||||
foreach (var metadataItem in listOfMetadata)
|
|
||||||
{
|
|
||||||
// Debug.WriteLine($"modifiableMetadata.RemoveQuerySafe(\"{metadataItem.metadataPath}\");");
|
|
||||||
Debug.WriteLine($"{metadataItem.MetadataPath} | {metadataItem.Value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all metadata
|
|
||||||
/// Iterates recursively through all metadata
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Intended for debug only!!!
|
|
||||||
/// </remarks>
|
|
||||||
public static List<(string MetadataPath, object Value)> GetListOfMetadataForDebug(this BitmapMetadata metadata)
|
|
||||||
{
|
|
||||||
var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GetMetadataRecursively(metadata, string.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOfAllMetadata;
|
|
||||||
|
|
||||||
void GetMetadataRecursively(BitmapMetadata metadata, string query)
|
|
||||||
{
|
|
||||||
if (metadata == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string relativeQuery in metadata)
|
|
||||||
{
|
|
||||||
string absolutePath = query + relativeQuery;
|
|
||||||
|
|
||||||
object metadataQueryReader = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
metadataQueryReader = metadata.GetQuerySafe(relativeQuery);
|
|
||||||
listOfAllMetadata.Add((absolutePath, metadataQueryReader));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
listOfAllMetadata.Add((absolutePath, $"######## INVALID METADATA: {ex.Message}"));
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataQueryReader is BitmapMetadata innerMetadata)
|
|
||||||
{
|
|
||||||
GetMetadataRecursively(innerMetadata, absolutePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
namespace System.Collections.Generic
|
namespace System.Collections.Generic
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
namespace System
|
|
||||||
{
|
|
||||||
internal static class TimeSpanExtensions
|
|
||||||
{
|
|
||||||
public static TimeSpan Multiply(this TimeSpan timeSpan, double scalar)
|
|
||||||
=> new TimeSpan((long)(timeSpan.Ticks * scalar));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// 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.ComponentModel;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace ImageResizer.Helpers
|
|
||||||
{
|
|
||||||
public class Observable : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
|
||||||
|
|
||||||
protected void Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
|
|
||||||
{
|
|
||||||
if (Equals(storage, value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage = value;
|
|
||||||
OnPropertyChanged(propertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// 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.Input;
|
|
||||||
|
|
||||||
namespace ImageResizer.Helpers
|
|
||||||
{
|
|
||||||
public class RelayCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly Action _execute;
|
|
||||||
private readonly Func<bool> _canExecute;
|
|
||||||
|
|
||||||
public event EventHandler CanExecuteChanged;
|
|
||||||
|
|
||||||
public RelayCommand(Action execute)
|
|
||||||
: this(execute, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelayCommand(Action execute, Func<bool> canExecute)
|
|
||||||
{
|
|
||||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
|
||||||
_canExecute = canExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
|
|
||||||
|
|
||||||
public void Execute(object parameter) => _execute();
|
|
||||||
|
|
||||||
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")]
|
|
||||||
public class RelayCommand<T> : ICommand
|
|
||||||
{
|
|
||||||
private readonly Action<T> execute;
|
|
||||||
|
|
||||||
private readonly Func<T, bool> canExecute;
|
|
||||||
|
|
||||||
public event EventHandler CanExecuteChanged;
|
|
||||||
|
|
||||||
public RelayCommand(Action<T> execute)
|
|
||||||
: this(execute, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
|
|
||||||
{
|
|
||||||
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
|
||||||
this.canExecute = canExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExecute(object parameter) => canExecute == null || canExecute((T)parameter);
|
|
||||||
|
|
||||||
public void Execute(object parameter) => execute((T)parameter);
|
|
||||||
|
|
||||||
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
using Microsoft.Windows.ApplicationModel.Resources;
|
||||||
|
|
||||||
|
namespace ImageResizer.Helpers
|
||||||
|
{
|
||||||
|
internal static class ResourceLoaderInstance
|
||||||
|
{
|
||||||
|
private static Func<string, string> _getString;
|
||||||
|
|
||||||
|
internal static Func<string, string> GetString
|
||||||
|
{
|
||||||
|
get => _getString ??= CreateDefault();
|
||||||
|
set => _getString = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<string, string> CreateDefault()
|
||||||
|
{
|
||||||
|
var loader = new ResourceLoader("PowerToys.ImageResizer.pri");
|
||||||
|
return loader.GetString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,78 +5,89 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyTitle>PowerToys.ImageResizer</AssemblyTitle>
|
<AssemblyTitle>PowerToys.ImageResizer</AssemblyTitle>
|
||||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
|
<AssemblyDescription>PowerToys Image Resizer</AssemblyDescription>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
|
||||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||||
<RootNamespace>ImageResizer</RootNamespace>
|
<RootNamespace>ImageResizer</RootNamespace>
|
||||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<UseWinUI>true</UseWinUI>
|
||||||
|
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
|
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<NoWarn>CA1863</NoWarn>
|
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||||
|
<ProjectPriFileName>PowerToys.ImageResizer.pri</ProjectPriFileName>
|
||||||
|
<!-- Custom Main entry point -->
|
||||||
|
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Remove="ImageResizerXAML\App.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
<NoWarn>0436;SA1210;SA1516;CA1305;CA1863;CA1852</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- <PropertyGroup>
|
<!-- Allow test project to access internal types -->
|
||||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ImageResizer.Test" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\ImageResizer\ImageResizer.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||||
|
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||||
|
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
|
||||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
|
||||||
</PropertyGroup> -->
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Properties\Resources.resx">
|
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Resources\ImageResizer.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Resources\ImageResizer.png" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||||
<PackageReference Include="System.CommandLine" />
|
<PackageReference Include="System.CommandLine" />
|
||||||
<PackageReference Include="System.IO.Abstractions" />
|
<PackageReference Include="System.IO.Abstractions" />
|
||||||
<PackageReference Include="WPF-UI" />
|
<PackageReference Include="WinUIEx" />
|
||||||
|
|
||||||
|
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
|
||||||
|
<PackageReference Include="Microsoft.Web.WebView2" />
|
||||||
|
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
|
||||||
|
<PackageReference Include="CommunityToolkit.Common" />
|
||||||
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||||
|
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||||
|
package has not yet been restored -->
|
||||||
|
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
|
||||||
|
<ProjectCapability Include="Msix" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
|
<Folder Include="Properties\" />
|
||||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||||
|
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Properties\Resources.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Ensure Resources directory and ImageResizer.png are available for dependent projects -->
|
|
||||||
<Target Name="CopyResourcesToSharedLocation" AfterTargets="Build">
|
|
||||||
<ItemGroup>
|
|
||||||
<ResourceFiles Include="$(MSBuildProjectDirectory)\Resources\ImageResizer.png" />
|
|
||||||
</ItemGroup>
|
|
||||||
<MakeDir Directories="$(OutputPath)Resources" Condition="!Exists('$(OutputPath)Resources')" />
|
|
||||||
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(OutputPath)Resources" SkipUnchangedFiles="true" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
|
|
||||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
|
||||||
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
|
|
||||||
packageName="Microsoft.PowerToys.SparseApp"
|
|
||||||
applicationId="PowerToys.ImageResizerUI" />
|
|
||||||
</assembly>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
|
|
||||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
|
||||||
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
|
||||||
packageName="Microsoft.PowerToys.SparseApp"
|
|
||||||
applicationId="PowerToys.ImageResizerUI" />
|
|
||||||
</assembly>
|
|
||||||
33
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml
Normal file
33
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Application
|
||||||
|
x:Class="ImageResizer.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="using:ImageResizer.Converters"
|
||||||
|
xmlns:local="using:ImageResizer"
|
||||||
|
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<!-- Layout tokens -->
|
||||||
|
<x:Double x:Key="ButtonMinWidth">76</x:Double>
|
||||||
|
<Thickness x:Key="PageSectionMargin">12,12,12,0</Thickness>
|
||||||
|
|
||||||
|
<!-- Converters -->
|
||||||
|
<converters:AutoDoubleConverter x:Key="AutoDoubleConverter" />
|
||||||
|
<tkconverters:BoolToVisibilityConverter
|
||||||
|
x:Key="BoolToVisibilityConverter"
|
||||||
|
FalseValue="Collapsed"
|
||||||
|
TrueValue="Visible" />
|
||||||
|
<converters:EnumToIntConverter x:Key="EnumToIntConverter" />
|
||||||
|
<converters:EnumValueConverter x:Key="EnumValueConverter" />
|
||||||
|
|
||||||
|
<tkconverters:BoolToVisibilityConverter
|
||||||
|
x:Key="InvertedBoolToVisibilityConverter"
|
||||||
|
FalseValue="Visible"
|
||||||
|
TrueValue="Collapsed" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
244
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml.cs
Normal file
244
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml.cs
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ImageResizer.Models;
|
||||||
|
using ImageResizer.Properties;
|
||||||
|
using ImageResizer.Services;
|
||||||
|
using ImageResizer.ViewModels;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace ImageResizer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides application-specific behavior to supplement the default Application class.
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : Application, IDisposable
|
||||||
|
{
|
||||||
|
private const string LogSubFolder = "\\Image Resizer\\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;
|
||||||
|
|
||||||
|
private Window _window;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="App"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string appLanguage = LanguageHelper.LoadLanguage();
|
||||||
|
if (!string.IsNullOrEmpty(appLanguage))
|
||||||
|
{
|
||||||
|
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Language initialization error: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.InitializeLogger(LogSubFolder);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Swallow logger init issues silently
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.InputEncoding = Encoding.Unicode;
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
UnhandledException += App_UnhandledException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the application is launched normally by the end user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Details about the launch request and process.</param>
|
||||||
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
// Initialize dispatcher for cross-thread property change notifications
|
||||||
|
Settings.InitializeDispatcher();
|
||||||
|
|
||||||
|
// Check GPO policy
|
||||||
|
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||||
|
Environment.Exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for AI detection mode (called by Runner in background)
|
||||||
|
var commandLineArgs = Environment.GetCommandLineArgs();
|
||||||
|
if (commandLineArgs?.Length > 1 && commandLineArgs[1] == "--detect-ai")
|
||||||
|
{
|
||||||
|
RunAiDetectionMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize AI availability
|
||||||
|
InitializeAiAvailability();
|
||||||
|
|
||||||
|
// Create batch from command line
|
||||||
|
var batch = ResizeBatch.FromCommandLine(Console.In, commandLineArgs);
|
||||||
|
|
||||||
|
// Create main window (not yet visible – HWND is available for the file picker)
|
||||||
|
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||||
|
_window = mainWindow;
|
||||||
|
|
||||||
|
mainWindow.DispatcherQueue.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
if (batch.Files.Count == 0)
|
||||||
|
{
|
||||||
|
// Show file picker before the window is visible
|
||||||
|
var files = await mainWindow.OpenPictureFilesAsync();
|
||||||
|
if (!files.Any())
|
||||||
|
{
|
||||||
|
Environment.Exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
batch.Files.Add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load ViewModel (sets page content).
|
||||||
|
// MainWindow will Activate itself after layout is settled to avoid flash.
|
||||||
|
await mainWindow.LoadViewModelAsync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeAiAvailability()
|
||||||
|
{
|
||||||
|
// AI Super Resolution is currently disabled
|
||||||
|
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
|
||||||
|
// If AI is enabled in the future, uncomment this section:
|
||||||
|
/*
|
||||||
|
// AI Super Resolution is not supported on Windows 10
|
||||||
|
if (OSVersionHelper.IsWindows10())
|
||||||
|
{
|
||||||
|
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Load AI availability from cache
|
||||||
|
var cachedState = AiAvailabilityCacheService.LoadCache();
|
||||||
|
|
||||||
|
if (cachedState.HasValue)
|
||||||
|
{
|
||||||
|
AiAvailabilityState = cachedState.Value;
|
||||||
|
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||||
|
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If AI is potentially available, start background initialization
|
||||||
|
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||||
|
{
|
||||||
|
_ = InitializeAiServiceAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI detection mode: perform detection, write to cache, and exit.
|
||||||
|
/// </summary>
|
||||||
|
private void RunAiDetectionMode()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogInfo("Running AI detection mode...");
|
||||||
|
|
||||||
|
// AI is currently disabled
|
||||||
|
AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||||
|
Logger.LogInfo("AI detection complete: NotSupported (feature disabled)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"AI detection failed: {ex.Message}");
|
||||||
|
AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize AI Super Resolution service asynchronously in background.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task InitializeAiServiceAsync()
|
||||||
|
{
|
||||||
|
AiAvailabilityState finalState;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||||
|
|
||||||
|
if (aiService != null)
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||||
|
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||||
|
finalState = AiAvailabilityState.Ready;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
|
||||||
|
finalState = AiAvailabilityState.NotSupported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||||
|
finalState = AiAvailabilityState.NotSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
AiAvailabilityState = finalState;
|
||||||
|
AiInitializationCompleted?.Invoke(null, finalState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unhandled exception", e.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ResizeBatch.DisposeAiSuperResolutionService();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml
Normal file
33
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||||
|
<!-- Licensed under the MIT License. -->
|
||||||
|
<winuiex:WindowEx
|
||||||
|
x:Class="ImageResizer.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:winuiex="using:WinUIEx"
|
||||||
|
MinHeight="1"
|
||||||
|
IsMaximizable="False"
|
||||||
|
IsMinimizable="False"
|
||||||
|
IsResizable="False"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Window.SystemBackdrop>
|
||||||
|
<MicaBackdrop />
|
||||||
|
</Window.SystemBackdrop>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TitleBar
|
||||||
|
x:Name="titleBar"
|
||||||
|
x:Uid="TitleBarTitle"
|
||||||
|
IsTabStop="False">
|
||||||
|
<TitleBar.IconSource>
|
||||||
|
<ImageIconSource ImageSource="/Assets/ImageResizer/ImageResizer.ico" />
|
||||||
|
</TitleBar.IconSource>
|
||||||
|
</TitleBar>
|
||||||
|
<ContentPresenter x:Name="contentPresenter" Grid.Row="1" />
|
||||||
|
</Grid>
|
||||||
|
</winuiex:WindowEx>
|
||||||
294
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml.cs
Normal file
294
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ImageResizer.ViewModels;
|
||||||
|
using ImageResizer.Views;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Windows.Graphics;
|
||||||
|
using Windows.Storage.Pickers;
|
||||||
|
using WinUIEx;
|
||||||
|
|
||||||
|
namespace ImageResizer
|
||||||
|
{
|
||||||
|
public sealed partial class MainWindow : WindowEx, IMainView
|
||||||
|
{
|
||||||
|
private const int MinWindowWidth = 460;
|
||||||
|
private const int InitialWindowHeight = 1;
|
||||||
|
private const double MaxWindowHeightScreenFraction = 0.85;
|
||||||
|
|
||||||
|
private bool _isFirstShow = true;
|
||||||
|
|
||||||
|
public MainViewModel ViewModel { get; }
|
||||||
|
|
||||||
|
private PropertyChangedEventHandler _selectedSizeChangedHandler;
|
||||||
|
private InputViewModel _currentInputViewModel;
|
||||||
|
|
||||||
|
public MainWindow(MainViewModel viewModel)
|
||||||
|
{
|
||||||
|
ViewModel = viewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
ExtendsContentIntoTitleBar = true;
|
||||||
|
SetTitleBar(titleBar);
|
||||||
|
this.SetIcon("Assets/ImageResizer/ImageResizer.ico");
|
||||||
|
|
||||||
|
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
|
||||||
|
|
||||||
|
// Keep the window hidden until content is loaded and measured.
|
||||||
|
// A tiny provisional height avoids stretching the page during the first layout pass.
|
||||||
|
this.SetWindowSize(MinWindowWidth, InitialWindowHeight);
|
||||||
|
AppWindow.Hide();
|
||||||
|
|
||||||
|
// Listen to ViewModel property changes
|
||||||
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadViewModelAsync()
|
||||||
|
{
|
||||||
|
await ViewModel.LoadAsync(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ViewModel.CurrentPage))
|
||||||
|
{
|
||||||
|
UpdateCurrentPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCurrentPage()
|
||||||
|
{
|
||||||
|
var page = ViewModel.CurrentPage;
|
||||||
|
if (page == null)
|
||||||
|
{
|
||||||
|
contentPresenter.Content = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page is InputViewModel inputVM)
|
||||||
|
{
|
||||||
|
var inputPage = new InputPage { ViewModel = inputVM };
|
||||||
|
contentPresenter.Content = inputPage;
|
||||||
|
|
||||||
|
AdjustWindowForInputPage(inputVM, inputPage);
|
||||||
|
}
|
||||||
|
else if (page is ProgressViewModel progressVM)
|
||||||
|
{
|
||||||
|
var progressPage = new ProgressPage { ViewModel = progressVM };
|
||||||
|
contentPresenter.Content = progressPage;
|
||||||
|
|
||||||
|
SizeAndShowOnLoaded(progressPage);
|
||||||
|
}
|
||||||
|
else if (page is ResultsViewModel resultsVM)
|
||||||
|
{
|
||||||
|
var resultsPage = new ResultsPage { ViewModel = resultsVM };
|
||||||
|
contentPresenter.Content = resultsPage;
|
||||||
|
|
||||||
|
SizeAndShowOnLoaded(resultsPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// After the element completes layout, size the window to fit and show it.
|
||||||
|
/// </summary>
|
||||||
|
private void SizeAndShowOnLoaded(FrameworkElement element)
|
||||||
|
{
|
||||||
|
void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
element.Loaded -= OnLoaded;
|
||||||
|
SizeToContent();
|
||||||
|
ShowWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustWindowForInputPage(InputViewModel inputVM, InputPage inputPage)
|
||||||
|
{
|
||||||
|
// Unsubscribe previous handler to prevent memory leak
|
||||||
|
if (_selectedSizeChangedHandler != null && _currentInputViewModel?.Settings != null)
|
||||||
|
{
|
||||||
|
_currentInputViewModel.Settings.PropertyChanged -= _selectedSizeChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentInputViewModel = inputVM;
|
||||||
|
|
||||||
|
// Create and store handler reference for future cleanup
|
||||||
|
_selectedSizeChangedHandler = (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(inputVM.Settings.SelectedSizeIndex))
|
||||||
|
{
|
||||||
|
// Content visibility changes after the selected size option changes;
|
||||||
|
// listen for the next LayoutUpdated to re-measure once layout settles.
|
||||||
|
SizeToContentAfterLayout(inputPage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inputVM.Settings.PropertyChanged += _selectedSizeChangedHandler;
|
||||||
|
|
||||||
|
SizeAndShowOnLoaded(inputPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activate and center the window on first show; subsequent calls are no-ops.
|
||||||
|
/// </summary>
|
||||||
|
private void ShowWindow()
|
||||||
|
{
|
||||||
|
if (_isFirstShow)
|
||||||
|
{
|
||||||
|
_isFirstShow = false;
|
||||||
|
this.CenterOnScreen();
|
||||||
|
this.Show();
|
||||||
|
Activate();
|
||||||
|
|
||||||
|
// Compact the visible window after the first shown layout pass.
|
||||||
|
// This trims any slack that remains after hidden-state sizing.
|
||||||
|
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||||
|
{
|
||||||
|
CompactWindowToRenderedContent();
|
||||||
|
this.CenterOnScreen();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sizes the window after the next layout pass completes.
|
||||||
|
/// Used when content changes on an already-loaded element (e.g., visibility toggles).
|
||||||
|
/// LayoutUpdated fires once per layout pass, so we unsubscribe immediately.
|
||||||
|
/// </summary>
|
||||||
|
private void SizeToContentAfterLayout(FrameworkElement element)
|
||||||
|
{
|
||||||
|
void OnLayoutUpdated(object sender, object e)
|
||||||
|
{
|
||||||
|
element.LayoutUpdated -= OnLayoutUpdated;
|
||||||
|
SizeToContent();
|
||||||
|
|
||||||
|
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, CompactWindowToRenderedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.LayoutUpdated += OnLayoutUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WinUI3 has no built-in SizeToContent (unlike WPF).
|
||||||
|
/// Measure the title bar and current page content at the current client width with
|
||||||
|
/// unconstrained height, then resize the client area to match.
|
||||||
|
///
|
||||||
|
/// Measuring the Page itself can over-report height because the Page may already be
|
||||||
|
/// stretched to the window's provisional size from the first layout pass.
|
||||||
|
/// </summary>
|
||||||
|
private void SizeToContent()
|
||||||
|
{
|
||||||
|
var pageContentRoot = GetCurrentPageContentRoot();
|
||||||
|
if (pageContentRoot == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scale = this.GetDpiForWindow() / 96.0;
|
||||||
|
var clientWidth = AppWindow.ClientSize.Width;
|
||||||
|
var availableWidth = clientWidth / scale;
|
||||||
|
|
||||||
|
titleBar.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||||
|
pageContentRoot.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||||
|
|
||||||
|
var desiredHeight = titleBar.DesiredSize.Height + pageContentRoot.DesiredSize.Height;
|
||||||
|
|
||||||
|
if (desiredHeight <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyWindowSizeForClientContent(desiredHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrameworkElement GetCurrentPageContentRoot()
|
||||||
|
{
|
||||||
|
if (contentPresenter.Content is Page page)
|
||||||
|
{
|
||||||
|
return page.Content as FrameworkElement ?? page;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentPresenter.Content as FrameworkElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompactWindowToRenderedContent()
|
||||||
|
{
|
||||||
|
var pageContentRoot = GetCurrentPageContentRoot();
|
||||||
|
var windowContentRoot = this.Content as FrameworkElement;
|
||||||
|
if (pageContentRoot == null || windowContentRoot == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRenderedHeight = windowContentRoot.ActualHeight;
|
||||||
|
var occupiedHeight = titleBar.ActualHeight + pageContentRoot.ActualHeight;
|
||||||
|
var slackHeight = totalRenderedHeight - occupiedHeight;
|
||||||
|
|
||||||
|
if (slackHeight <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reducedHeight = totalRenderedHeight - slackHeight;
|
||||||
|
|
||||||
|
if (reducedHeight <= 0 || reducedHeight >= totalRenderedHeight)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyWindowSizeForClientContent(reducedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyWindowSizeForClientContent(double desiredClientHeight)
|
||||||
|
{
|
||||||
|
var scale = this.GetDpiForWindow() / 96.0;
|
||||||
|
var frameHeight = Math.Max(0, AppWindow.Size.Height - AppWindow.ClientSize.Height) / scale;
|
||||||
|
var outerHeight = desiredClientHeight + frameHeight;
|
||||||
|
|
||||||
|
var displayArea = Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(AppWindow.Id, Microsoft.UI.Windowing.DisplayAreaFallback.Nearest);
|
||||||
|
var maxScreenHeight = displayArea.WorkArea.Height / scale * MaxWindowHeightScreenFraction;
|
||||||
|
outerHeight = Math.Min(outerHeight, maxScreenHeight);
|
||||||
|
|
||||||
|
this.SetWindowSize(MinWindowWidth, outerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<string>> OpenPictureFilesAsync()
|
||||||
|
{
|
||||||
|
var picker = this.CreateOpenFilePicker();
|
||||||
|
picker.ViewMode = PickerViewMode.Thumbnail;
|
||||||
|
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
|
||||||
|
string[] imageExtensions = [".bmp", ".dib", ".exif", ".gif", ".jfif", ".jpe",
|
||||||
|
".jpeg", ".jpg", ".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp"];
|
||||||
|
|
||||||
|
foreach (var ext in imageExtensions)
|
||||||
|
{
|
||||||
|
picker.FileTypeFilter.Add(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = await picker.PickMultipleFilesAsync();
|
||||||
|
if (files != null && files.Count > 0)
|
||||||
|
{
|
||||||
|
return files.Select(f => f.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void IMainView.Close()
|
||||||
|
{
|
||||||
|
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ImageResizer.Views
|
||||||
|
{
|
||||||
|
public interface IMainView
|
||||||
|
{
|
||||||
|
Task<IEnumerable<string>> OpenPictureFilesAsync();
|
||||||
|
|
||||||
|
void Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||||
|
<!-- Licensed under the MIT License. -->
|
||||||
|
<Page
|
||||||
|
x:Class="ImageResizer.Views.InputPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="using:ImageResizer.Views"
|
||||||
|
xmlns:m="using:ImageResizer.Models"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Page.Resources>
|
||||||
|
<!-- Template for normal ResizeSize presets (Small, Medium, Large, Phone) -->
|
||||||
|
<DataTemplate x:Key="ResizeSizeTemplate" x:DataType="m:ResizeSize">
|
||||||
|
<Grid VerticalAlignment="Center" AutomationProperties.Name="{x:Bind Name}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="×"
|
||||||
|
Visibility="{x:Bind ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||||
|
Visibility="{x:Bind ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
<TextBlock Margin="4,0,0,0" Text="{x:Bind Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Template for CustomSize - shows only name -->
|
||||||
|
<DataTemplate x:Key="CustomSizeTemplate" x:DataType="m:CustomSize">
|
||||||
|
<Grid VerticalAlignment="Center" AutomationProperties.Name="{x:Bind Name}">
|
||||||
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Template for AiSize - shows name and description -->
|
||||||
|
<DataTemplate x:Key="AiSizeTemplate" x:DataType="m:AiSize">
|
||||||
|
<StackPanel
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.Name="{x:Bind Name}"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||||
|
<TextBlock x:Uid="Input_AiSuperResolutionDescription" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- DataTemplateSelector to choose the right template based on item type -->
|
||||||
|
<local:SizeDataTemplateSelector
|
||||||
|
x:Key="SizeTemplateSelector"
|
||||||
|
AiSizeTemplate="{StaticResource AiSizeTemplate}"
|
||||||
|
CustomSizeTemplate="{StaticResource CustomSizeTemplate}"
|
||||||
|
ResizeSizeTemplate="{StaticResource ResizeSizeTemplate}" />
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
|
<Grid VerticalAlignment="Top">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Margin="16">
|
||||||
|
<ComboBox
|
||||||
|
x:Name="SizeComboBox"
|
||||||
|
MinHeight="64"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
VerticalContentAlignment="Stretch"
|
||||||
|
ItemTemplateSelector="{StaticResource SizeTemplateSelector}"
|
||||||
|
ItemsSource="{x:Bind ViewModel.Settings.AllSizes, Mode=OneWay}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.Settings.SelectedSizeIndex, Mode=TwoWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Content area with gray background (AI/Custom panels + checkboxes) -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,1,0,1" />
|
||||||
|
|
||||||
|
<!-- AI Configuration Panel -->
|
||||||
|
<Grid
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16,16,16,8"
|
||||||
|
Visibility="{x:Bind ViewModel.ShowAiConfigurationSection, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<!-- AI Model Download Prompt -->
|
||||||
|
<StackPanel Visibility="{x:Bind ViewModel.ShowModelDownloadPrompt, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<InfoBar
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="{x:Bind ViewModel.ModelStatusMessage, Mode=OneWay}"
|
||||||
|
Severity="Informational" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{x:Bind ViewModel.DownloadModelCommand}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}"
|
||||||
|
Visibility="{x:Bind ViewModel.IsDownloadingModel, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}}">
|
||||||
|
<TextBlock x:Uid="Input_AiModelDownloadButton" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<StackPanel Margin="0,8,0,0" Visibility="{x:Bind ViewModel.IsDownloadingModel, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<ProgressBar IsIndeterminate="True" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="{x:Bind ViewModel.ModelStatusMessage, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- AI Scale Controls -->
|
||||||
|
<StackPanel Visibility="{x:Bind ViewModel.ShowAiControls, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<Grid>
|
||||||
|
<TextBlock x:Uid="Input_AiCurrentLabel" />
|
||||||
|
<TextBlock HorizontalAlignment="Right" Text="{x:Bind ViewModel.AiScaleDisplay, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Maximum="8"
|
||||||
|
Minimum="1"
|
||||||
|
TickFrequency="1"
|
||||||
|
TickPlacement="BottomRight"
|
||||||
|
Value="{x:Bind ViewModel.AiSuperResolutionScale, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<StackPanel Margin="0,16,0,0" Visibility="{x:Bind ViewModel.ShowAiSizeDescriptions, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<Grid>
|
||||||
|
<TextBlock x:Uid="Input_AiCurrentLabel" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{x:Bind ViewModel.CurrentResolutionDescription, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
<Grid Margin="0,8,0,0">
|
||||||
|
<TextBlock x:Uid="Input_AiNewLabel" />
|
||||||
|
<TextBlock HorizontalAlignment="Right" Text="{x:Bind ViewModel.NewResolutionDescription, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Custom input matrix -->
|
||||||
|
<Grid
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16,16,16,8"
|
||||||
|
ColumnSpacing="8"
|
||||||
|
RowSpacing="8"
|
||||||
|
Visibility="{x:Bind ViewModel.IsCustomSizeSelected, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
AutomationProperties.Name="Width"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<NumberBox
|
||||||
|
x:Name="WidthNumberBox"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
KeyDown="NumberBox_KeyDown"
|
||||||
|
Minimum="0"
|
||||||
|
SpinButtonPlacementMode="Inline"
|
||||||
|
Value="{x:Bind ViewModel.Settings.CustomSize.Width, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
AutomationProperties.Name="Height"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph=""
|
||||||
|
RenderTransformOrigin="0.5,0.5"
|
||||||
|
Visibility="{x:Bind ViewModel.Settings.CustomSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
|
<FontIcon.RenderTransform>
|
||||||
|
<RotateTransform Angle="90" />
|
||||||
|
</FontIcon.RenderTransform>
|
||||||
|
</FontIcon>
|
||||||
|
<NumberBox
|
||||||
|
x:Name="HeightNumberBox"
|
||||||
|
Grid.Column="3"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
KeyDown="NumberBox_KeyDown"
|
||||||
|
Minimum="0"
|
||||||
|
SpinButtonPlacementMode="Inline"
|
||||||
|
Visibility="{x:Bind ViewModel.Settings.CustomSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
|
Value="{x:Bind ViewModel.Settings.CustomSize.Height, Mode=TwoWay}" />
|
||||||
|
<FontIcon
|
||||||
|
Grid.Row="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<ComboBox
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ResizeFitValues, Mode=OneWay}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.Settings.CustomSize.Fit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="m:ResizeFit">
|
||||||
|
<TextBlock Padding="2,0" Text="{x:Bind Converter={StaticResource EnumValueConverter}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<FontIcon
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Glyph="" />
|
||||||
|
<ComboBox
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="3"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ResizeUnitValues, Mode=OneWay}"
|
||||||
|
SelectedIndex="{x:Bind ViewModel.Settings.CustomSize.Unit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="m:ResizeUnit">
|
||||||
|
<TextBlock Padding="2,0" Text="{x:Bind Converter={StaticResource EnumValueConverter}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- CheckBoxes -->
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="16,8,16,16"
|
||||||
|
Orientation="Vertical">
|
||||||
|
<CheckBox IsChecked="{x:Bind ViewModel.Settings.ShrinkOnly, Mode=TwoWay}">
|
||||||
|
<TextBlock x:Uid="Input_ShrinkOnly" TextWrapping="Wrap" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{x:Bind ViewModel.Settings.IgnoreOrientation, Mode=TwoWay}">
|
||||||
|
<TextBlock x:Uid="Input_IgnoreOrientation" TextWrapping="Wrap" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{x:Bind ViewModel.Settings.Replace, Mode=TwoWay}">
|
||||||
|
<TextBlock x:Uid="Input_Replace" TextWrapping="Wrap" />
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{x:Bind ViewModel.Settings.RemoveMetadata, Mode=TwoWay}">
|
||||||
|
<TextBlock x:Uid="Input_RemoveMetadata" TextWrapping="Wrap" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- GIF warning (outside gray area) -->
|
||||||
|
<InfoBar
|
||||||
|
x:Uid="Input_GifWarning"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="16,8,16,0"
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="True"
|
||||||
|
Severity="Warning"
|
||||||
|
Visibility="{x:Bind ViewModel.HasGifFiles, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
|
||||||
|
<!-- Buttons (outside gray area) -->
|
||||||
|
<Grid Grid.Row="3" Margin="8,16,16,16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
x:Uid="Open_settings_button"
|
||||||
|
Padding="8"
|
||||||
|
Command="{x:Bind ViewModel.OpenSettingsCommand}"
|
||||||
|
Style="{StaticResource SubtleButtonStyle}">
|
||||||
|
<FontIcon FontSize="16" Glyph="" />
|
||||||
|
<ToolTipService.ToolTip>
|
||||||
|
<TextBlock x:Uid="Open_settings" />
|
||||||
|
</ToolTipService.ToolTip>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Column="1"
|
||||||
|
MinWidth="{StaticResource ButtonMinWidth}"
|
||||||
|
Command="{x:Bind ViewModel.ResizeCommand}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}">
|
||||||
|
<Button.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Enter" Invoked="ResizeAccelerator_Invoked" />
|
||||||
|
</Button.KeyboardAccelerators>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<FontIcon FontSize="14" Glyph="" />
|
||||||
|
<TextBlock x:Uid="Input_Resize" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
MinWidth="{StaticResource ButtonMinWidth}"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Command="{x:Bind ViewModel.CancelCommand}">
|
||||||
|
<Button.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Escape" />
|
||||||
|
</Button.KeyboardAccelerators>
|
||||||
|
<TextBlock x:Uid="Cancel" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
|
using ImageResizer.ViewModels;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
|
||||||
|
using static ImageResizer.ViewModels.InputViewModel;
|
||||||
|
|
||||||
|
namespace ImageResizer.Views
|
||||||
|
{
|
||||||
|
public sealed partial class InputPage : Page
|
||||||
|
{
|
||||||
|
public InputViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
public InputPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResizeAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||||
|
{
|
||||||
|
if (FocusManager.GetFocusedElement(XamlRoot) is NumberBox)
|
||||||
|
{
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NumberBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Windows.System.VirtualKey.Enter
|
||||||
|
&& sender is NumberBox numberBox
|
||||||
|
&& ViewModel is not null
|
||||||
|
&& !double.IsNaN(numberBox.Value))
|
||||||
|
{
|
||||||
|
KeyPressParams keyParams = numberBox.Name switch
|
||||||
|
{
|
||||||
|
"WidthNumberBox" => new KeyPressParams { Value = numberBox.Value, Dimension = Dimension.Width },
|
||||||
|
"HeightNumberBox" => new KeyPressParams { Value = numberBox.Value, Dimension = Dimension.Height },
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (keyParams is not null)
|
||||||
|
{
|
||||||
|
ViewModel.EnterKeyPressedCommand.Execute(keyParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||||
|
<!-- Licensed under the MIT License. -->
|
||||||
|
<Page
|
||||||
|
x:Class="ImageResizer.Views.ProgressPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
Loaded="Page_Loaded"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
x:Uid="Progress_MainInstruction"
|
||||||
|
Margin="{StaticResource PageSectionMargin}"
|
||||||
|
Style="{StaticResource SubtitleTextBlockStyle}" />
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource PageSectionMargin}"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{x:Bind ViewModel.TimeRemainingDisplay, Mode=OneWay}" />
|
||||||
|
<ProgressBar
|
||||||
|
Height="16"
|
||||||
|
Margin="{StaticResource PageSectionMargin}"
|
||||||
|
Maximum="1"
|
||||||
|
Value="{x:Bind ViewModel.Progress, Mode=OneWay}" />
|
||||||
|
<Border
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
|
<Button MinWidth="{StaticResource ButtonMinWidth}" Command="{x:Bind ViewModel.StopCommand}">
|
||||||
|
<Button.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Escape" />
|
||||||
|
</Button.KeyboardAccelerators>
|
||||||
|
<TextBlock x:Uid="Progress_Stop" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Page>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
|
using ImageResizer.ViewModels;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ImageResizer.Views
|
||||||
|
{
|
||||||
|
public sealed partial class ProgressPage : Page
|
||||||
|
{
|
||||||
|
public ProgressViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
public ProgressPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel?.StartCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||||
|
<!-- Licensed under the MIT License. -->
|
||||||
|
<Page
|
||||||
|
x:Class="ImageResizer.Views.ResultsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:m="using:ImageResizer.Models"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
x:Uid="Results_MainInstruction"
|
||||||
|
Margin="{StaticResource PageSectionMargin}"
|
||||||
|
Style="{StaticResource SubtitleTextBlockStyle}" />
|
||||||
|
|
||||||
|
<ScrollViewer
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl Margin="12,4,12,0" ItemsSource="{x:Bind ViewModel.Errors, Mode=OneWay}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="m:ResizeError">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Text="{x:Bind File}" />
|
||||||
|
<TextBlock Text="{x:Bind Error}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
|
<Button MinWidth="{StaticResource ButtonMinWidth}" Command="{x:Bind ViewModel.CloseCommand}">
|
||||||
|
<Button.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Escape" />
|
||||||
|
</Button.KeyboardAccelerators>
|
||||||
|
<TextBlock x:Uid="Results_Close" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
|
using ImageResizer.ViewModels;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ImageResizer.Views
|
||||||
|
{
|
||||||
|
public sealed partial class ResultsPage : Page
|
||||||
|
{
|
||||||
|
public ResultsViewModel ViewModel { get; set; }
|
||||||
|
|
||||||
|
public ResultsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 ImageResizer.Models;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ImageResizer.Views
|
||||||
|
{
|
||||||
|
public partial class SizeDataTemplateSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate ResizeSizeTemplate { get; set; }
|
||||||
|
public DataTemplate CustomSizeTemplate { get; set; }
|
||||||
|
public DataTemplate AiSizeTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||||
|
{
|
||||||
|
if (item is AiSize)
|
||||||
|
{
|
||||||
|
return AiSizeTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is CustomSize)
|
||||||
|
{
|
||||||
|
return CustomSizeTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is ResizeSize)
|
||||||
|
{
|
||||||
|
return ResizeSizeTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.SelectTemplateCore(item, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,31 @@
|
|||||||
// Copyright (c) Microsoft Corporation
|
// Copyright (c) Microsoft Corporation
|
||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Helpers;
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
public class AiSize : ResizeSize
|
public partial class AiSize : ResizeSize
|
||||||
{
|
{
|
||||||
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
|
private static CompositeFormat _scaleFormat;
|
||||||
|
|
||||||
|
private static CompositeFormat ScaleFormat =>
|
||||||
|
_scaleFormat ??= CompositeFormat.Parse(ResourceLoaderInstance.GetString("Input_AiScaleFormat"));
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("scale")]
|
||||||
private int _scale = 2;
|
private int _scale = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the formatted scale display string (e.g., "2×").
|
/// Gets the formatted scale display string (e.g., "2x").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
|
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, Scale);
|
||||||
|
|
||||||
[JsonPropertyName("scale")]
|
|
||||||
public int Scale
|
|
||||||
{
|
|
||||||
get => _scale;
|
|
||||||
set => Set(ref _scale, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public AiSize(int scale)
|
public AiSize(int scale)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.CommandLine.Parsing;
|
using System.CommandLine.Parsing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using ImageResizer.Cli.Commands;
|
using ImageResizer.Cli.Commands;
|
||||||
|
using ImageResizer.Helpers;
|
||||||
|
|
||||||
#pragma warning disable SA1649 // File name should match first type name
|
#pragma warning disable SA1649 // File name should match first type name
|
||||||
#pragma warning disable SA1402 // File may only contain a single type
|
#pragma warning disable SA1402 // File may only contain a single type
|
||||||
@@ -19,117 +20,51 @@ namespace ImageResizer.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CliOptions
|
public class CliOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to show help information.
|
|
||||||
/// </summary>
|
|
||||||
public bool ShowHelp { get; set; }
|
public bool ShowHelp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to show current configuration.
|
|
||||||
/// </summary>
|
|
||||||
public bool ShowConfig { get; set; }
|
public bool ShowConfig { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the destination directory for resized images.
|
|
||||||
/// </summary>
|
|
||||||
public string DestinationDirectory { get; set; }
|
public string DestinationDirectory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the width of the resized image.
|
|
||||||
/// </summary>
|
|
||||||
public double? Width { get; set; }
|
public double? Width { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the height of the resized image.
|
|
||||||
/// </summary>
|
|
||||||
public double? Height { get; set; }
|
public double? Height { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
|
|
||||||
/// </summary>
|
|
||||||
public ResizeUnit? Unit { get; set; }
|
public ResizeUnit? Unit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
|
|
||||||
/// </summary>
|
|
||||||
public ResizeFit? Fit { get; set; }
|
public ResizeFit? Fit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the index of the preset size to use.
|
|
||||||
/// </summary>
|
|
||||||
public int? SizeIndex { get; set; }
|
public int? SizeIndex { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
|
|
||||||
/// </summary>
|
|
||||||
public bool? ShrinkOnly { get; set; }
|
public bool? ShrinkOnly { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to replace the original file.
|
|
||||||
/// </summary>
|
|
||||||
public bool? Replace { get; set; }
|
public bool? Replace { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to ignore orientation when resizing.
|
|
||||||
/// </summary>
|
|
||||||
public bool? IgnoreOrientation { get; set; }
|
public bool? IgnoreOrientation { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to remove metadata from the resized image.
|
|
||||||
/// </summary>
|
|
||||||
public bool? RemoveMetadata { get; set; }
|
public bool? RemoveMetadata { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the JPEG quality level (1-100).
|
|
||||||
/// </summary>
|
|
||||||
public int? JpegQualityLevel { get; set; }
|
public int? JpegQualityLevel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to keep the date modified.
|
|
||||||
/// </summary>
|
|
||||||
public bool? KeepDateModified { get; set; }
|
public bool? KeepDateModified { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the output filename format.
|
|
||||||
/// </summary>
|
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
|
|
||||||
/// </summary>
|
|
||||||
public bool? ProgressLines { get; set; }
|
public bool? ProgressLines { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of files to process.
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<string> Files { get; } = new List<string>();
|
public ICollection<string> Files { get; } = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the pipe name for receiving file list.
|
|
||||||
/// </summary>
|
|
||||||
public string PipeName { get; set; }
|
public string PipeName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
public IReadOnlyList<string> ParseErrors { get; private set; } = [];
|
||||||
/// Gets parse/validation errors produced by System.CommandLine.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a boolean value to nullable bool (true -> true, false -> null).
|
|
||||||
/// </summary>
|
|
||||||
private static bool? ToBoolOrNull(bool value) => value ? true : null;
|
private static bool? ToBoolOrNull(bool value) => value ? true : null;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses command-line arguments into CliOptions using System.CommandLine.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">The command-line arguments.</param>
|
|
||||||
/// <returns>A CliOptions instance with parsed values.</returns>
|
|
||||||
public static CliOptions Parse(string[] args)
|
public static CliOptions Parse(string[] args)
|
||||||
{
|
{
|
||||||
var options = new CliOptions();
|
var options = new CliOptions();
|
||||||
var cmd = new ImageResizerRootCommand();
|
var cmd = new ImageResizerRootCommand();
|
||||||
|
|
||||||
// Parse using System.CommandLine
|
|
||||||
var parseResult = new Parser(cmd).Parse(args);
|
var parseResult = new Parser(cmd).Parse(args);
|
||||||
|
|
||||||
if (parseResult.Errors.Count > 0)
|
if (parseResult.Errors.Count > 0)
|
||||||
@@ -143,7 +78,6 @@ namespace ImageResizer.Models
|
|||||||
options.ParseErrors = new ReadOnlyCollection<string>(errors);
|
options.ParseErrors = new ReadOnlyCollection<string>(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract values from parse result using strongly typed options
|
|
||||||
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
|
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
|
||||||
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
|
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
|
||||||
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
|
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
|
||||||
@@ -153,7 +87,6 @@ namespace ImageResizer.Models
|
|||||||
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
|
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
|
||||||
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
|
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
|
||||||
|
|
||||||
// Convert bool to nullable bool (true -> true, false -> null)
|
|
||||||
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
|
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
|
||||||
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
|
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
|
||||||
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
|
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
|
||||||
@@ -165,14 +98,12 @@ namespace ImageResizer.Models
|
|||||||
|
|
||||||
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
|
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
|
||||||
|
|
||||||
// Get files from arguments
|
|
||||||
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
|
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
|
||||||
if (files != null)
|
if (files != null)
|
||||||
{
|
{
|
||||||
const string pipeNamePrefix = "\\\\.\\pipe\\";
|
const string pipeNamePrefix = "\\\\.\\pipe\\";
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
// Check for pipe name (must be at the start of the path)
|
|
||||||
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
|
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
options.PipeName = file.Substring(pipeNamePrefix.Length);
|
options.PipeName = file.Substring(pipeNamePrefix.Length);
|
||||||
@@ -187,62 +118,55 @@ namespace ImageResizer.Models
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints current configuration to the console.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settings">The settings to display.</param>
|
|
||||||
public static void PrintConfig(ImageResizer.Properties.Settings settings)
|
public static void PrintConfig(ImageResizer.Properties.Settings settings)
|
||||||
{
|
{
|
||||||
|
var getString = ResourceLoaderInstance.GetString;
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
|
Console.WriteLine(getString("CLI_ConfigTitle"));
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
|
Console.WriteLine(getString("CLI_ConfigGeneralSettings"));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigShrinkOnly"), settings.ShrinkOnly));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigReplaceOriginal"), settings.Replace));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigIgnoreOrientation"), settings.IgnoreOrientation));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigRemoveMetadata"), settings.RemoveMetadata));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigKeepDateModified"), settings.KeepDateModified));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigJpegQuality"), settings.JpegQualityLevel));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigPngInterlace"), settings.PngInterlaceOption));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigTiffCompress"), settings.TiffCompressOption));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigFilenameFormat"), settings.FileName));
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
|
Console.WriteLine(getString("CLI_ConfigCustomSize"));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigWidth"), settings.CustomSize.Width, settings.CustomSize.Unit));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigHeight"), settings.CustomSize.Height, settings.CustomSize.Unit));
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigFitMode"), settings.CustomSize.Fit));
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
|
Console.WriteLine(getString("CLI_ConfigPresetSizes"));
|
||||||
for (int i = 0; i < settings.Sizes.Count; i++)
|
for (int i = 0; i < settings.Sizes.Count; i++)
|
||||||
{
|
{
|
||||||
var size = settings.Sizes[i];
|
var size = settings.Sizes[i];
|
||||||
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
|
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigPresetSizeFormat"), i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
|
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
|
||||||
{
|
{
|
||||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
|
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, getString("CLI_ConfigCustomSelected"), settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints usage information to the console.
|
|
||||||
/// </summary>
|
|
||||||
public static void PrintUsage()
|
public static void PrintUsage()
|
||||||
{
|
{
|
||||||
|
var getString = ResourceLoaderInstance.GetString;
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
|
Console.WriteLine(getString("CLI_UsageTitle"));
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var cmd = new ImageResizerRootCommand();
|
var cmd = new ImageResizerRootCommand();
|
||||||
|
|
||||||
// Print usage line
|
Console.WriteLine(getString("CLI_UsageLine"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageLine);
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
// Print options from the command definition
|
Console.WriteLine(getString("CLI_UsageOptions"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
|
|
||||||
foreach (var option in cmd.Options)
|
foreach (var option in cmd.Options)
|
||||||
{
|
{
|
||||||
var aliases = string.Join(", ", option.Aliases);
|
var aliases = string.Join(", ", option.Aliases);
|
||||||
@@ -251,11 +175,11 @@ namespace ImageResizer.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
|
Console.WriteLine(getString("CLI_UsageExamples"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
|
Console.WriteLine(getString("CLI_UsageExampleHelp"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
|
Console.WriteLine(getString("CLI_UsageExampleDimensions"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
|
Console.WriteLine(getString("CLI_UsageExamplePercent"));
|
||||||
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
|
Console.WriteLine(getString("CLI_UsageExamplePreset"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Properties;
|
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
@@ -15,7 +14,7 @@ namespace ImageResizer.Models
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public override string Name
|
public override string Name
|
||||||
{
|
{
|
||||||
get => Resources.Input_Custom;
|
get => ResourceLoaderInstance.GetString("Input_Custom");
|
||||||
set { /* no-op */ }
|
set { /* no-op */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
src/modules/imageresizer/ui/Models/ImagingEnums.cs
Normal file
32
src/modules/imageresizer/ui/Models/ImagingEnums.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PNG interlace option for the encoder.
|
||||||
|
/// Integer values preserve backward compatibility with existing settings JSON.
|
||||||
|
/// </summary>
|
||||||
|
public enum PngInterlaceOption
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
On = 1,
|
||||||
|
Off = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TIFF compression option for the encoder.
|
||||||
|
/// Integer values preserve backward compatibility with existing settings JSON.
|
||||||
|
/// </summary>
|
||||||
|
public enum TiffCompressOption
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
None = 1,
|
||||||
|
Ccitt3 = 2,
|
||||||
|
Ccitt4 = 3,
|
||||||
|
Lzw = 4,
|
||||||
|
Rle = 5,
|
||||||
|
Zip = 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -10,11 +10,9 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
using ImageResizer.Services;
|
using ImageResizer.Services;
|
||||||
|
|
||||||
@@ -40,6 +38,12 @@ namespace ImageResizer.Models
|
|||||||
_aiSuperResolutionService = null;
|
_aiSuperResolutionService = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ValidImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
|
||||||
|
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates if a file path is a supported image format.
|
/// Validates if a file path is a supported image format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,14 +61,8 @@ namespace ImageResizer.Models
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = Path.GetExtension(path)?.ToLowerInvariant();
|
var ext = Path.GetExtension(path);
|
||||||
var validExtensions = new[]
|
return ValidImageExtensions.Contains(ext);
|
||||||
{
|
|
||||||
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
|
|
||||||
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
|
|
||||||
};
|
|
||||||
|
|
||||||
return validExtensions.Contains(ext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -120,7 +118,7 @@ namespace ImageResizer.Models
|
|||||||
{
|
{
|
||||||
string file;
|
string file;
|
||||||
|
|
||||||
// Display the read text to the console
|
// Read file paths from the named pipe
|
||||||
while ((file = sr.ReadLine()) != null)
|
while ((file = sr.ReadLine()) != null)
|
||||||
{
|
{
|
||||||
if (IsValidImagePath(file))
|
if (IsValidImagePath(file))
|
||||||
@@ -141,37 +139,35 @@ namespace ImageResizer.Models
|
|||||||
return FromCliOptions(standardInput, options);
|
return FromCliOptions(standardInput, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
|
public Task<IEnumerable<ResizeError>> ProcessAsync(Action<int, double> reportProgress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// NOTE: Settings.Default is captured once before parallel processing.
|
// NOTE: Settings.Default is captured once before parallel processing.
|
||||||
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
|
// Any changes to settings on disk during this batch will NOT be reflected until the next batch.
|
||||||
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
|
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
|
||||||
return Process(reportProgress, Settings.Default, cancellationToken);
|
return ProcessAsync(reportProgress, Settings.Default, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
|
public async Task<IEnumerable<ResizeError>> ProcessAsync(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
double total = Files.Count;
|
double total = Files.Count;
|
||||||
int completed = 0;
|
int completed = 0;
|
||||||
var errors = new ConcurrentBag<ResizeError>();
|
var errors = new ConcurrentBag<ResizeError>();
|
||||||
|
|
||||||
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
|
await Parallel.ForEachAsync(
|
||||||
// APIs and a custom SynchronizationContext
|
|
||||||
Parallel.ForEach(
|
|
||||||
Files,
|
Files,
|
||||||
new ParallelOptions
|
new ParallelOptions
|
||||||
{
|
{
|
||||||
CancellationToken = cancellationToken,
|
CancellationToken = cancellationToken,
|
||||||
},
|
},
|
||||||
(file, state, i) =>
|
async (file, ct) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Execute(file, settings);
|
await ExecuteAsync(file, settings);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errors.Add(new ResizeError { File = _fileSystem.Path.GetFileName(file), Error = ex.Message });
|
errors.Add(new ResizeError(_fileSystem.Path.GetFileName(file), ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Increment(ref completed);
|
Interlocked.Increment(ref completed);
|
||||||
@@ -181,10 +177,10 @@ namespace ImageResizer.Models
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Execute(string file, Settings settings)
|
protected virtual async Task ExecuteAsync(string file, Settings settings)
|
||||||
{
|
{
|
||||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||||
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
|
await new ResizeOperation(file, DestinationDirectory, settings, aiService).ExecuteAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
public class ResizeError
|
public record ResizeError(string File, string Error);
|
||||||
{
|
|
||||||
public string File { get; set; }
|
|
||||||
|
|
||||||
public string Error { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using Windows.Foundation;
|
||||||
using System.Windows.Media.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
|
using Windows.Storage.Streams;
|
||||||
using ImageResizer.Extensions;
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
using ImageResizer.Services;
|
using ImageResizer.Services;
|
||||||
using ImageResizer.Utilities;
|
using ImageResizer.Utilities;
|
||||||
using Microsoft.VisualBasic.FileIO;
|
using Microsoft.VisualBasic.FileIO;
|
||||||
|
|
||||||
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
|
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
@@ -35,15 +33,24 @@ namespace ImageResizer.Models
|
|||||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||||
|
|
||||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||||
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
|
private static CompositeFormat _aiErrorFormat;
|
||||||
|
|
||||||
|
private static CompositeFormat AiErrorFormat =>
|
||||||
|
_aiErrorFormat ??= CompositeFormat.Parse(ResourceLoaderInstance.GetString("Error_AiProcessingFailed"));
|
||||||
|
|
||||||
|
private static readonly string[] RenderingMetadataProperties =
|
||||||
|
[
|
||||||
|
"System.Photo.Orientation",
|
||||||
|
"System.Image.ColorSpace",
|
||||||
|
];
|
||||||
|
|
||||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||||
private static readonly string[] _avoidFilenames =
|
private static readonly string[] _avoidFilenames =
|
||||||
{
|
[
|
||||||
"CON", "PRN", "AUX", "NUL",
|
"CON", "PRN", "AUX", "NUL",
|
||||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||||
};
|
];
|
||||||
|
|
||||||
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
||||||
{
|
{
|
||||||
@@ -53,78 +60,83 @@ namespace ImageResizer.Models
|
|||||||
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute()
|
public async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
string path;
|
string path;
|
||||||
|
|
||||||
using (var inputStream = _fileSystem.File.OpenRead(_file))
|
using (var inputStream = _fileSystem.File.OpenRead(_file))
|
||||||
{
|
{
|
||||||
var decoder = BitmapDecoder.Create(
|
var winrtInputStream = inputStream.AsRandomAccessStream();
|
||||||
inputStream,
|
var decoder = await BitmapDecoder.CreateAsync(winrtInputStream);
|
||||||
BitmapCreateOptions.PreservePixelFormat,
|
|
||||||
BitmapCacheOption.None);
|
|
||||||
|
|
||||||
var containerFormat = decoder.CodecInfo.ContainerFormat;
|
// Determine encoder ID from decoder
|
||||||
|
var encoderId = CodecHelper.GetEncoderIdForDecoder(decoder);
|
||||||
var encoder = CreateEncoder(containerFormat);
|
if (encoderId == null || !CodecHelper.CanEncode(encoderId.Value))
|
||||||
|
|
||||||
if (decoder.Metadata != null)
|
|
||||||
{
|
{
|
||||||
try
|
encoderId = CodecHelper.GetEncoderIdFromLegacyGuid(_settings.FallbackEncoder);
|
||||||
{
|
|
||||||
encoder.Metadata = decoder.Metadata;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoder.Palette != null)
|
var encoderGuid = encoderId.Value;
|
||||||
|
|
||||||
|
if (_settings.SelectedSize is AiSize)
|
||||||
{
|
{
|
||||||
encoder.Palette = decoder.Palette;
|
path = await ExecuteAiAsync(decoder, winrtInputStream, encoderGuid);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
foreach (var originalFrame in decoder.Frames)
|
|
||||||
{
|
{
|
||||||
var transformedBitmap = Transform(originalFrame);
|
var originalWidth = (int)decoder.PixelWidth;
|
||||||
|
var originalHeight = (int)decoder.PixelHeight;
|
||||||
|
|
||||||
// if the frame was not modified, we should not replace the metadata
|
var (scaledWidth, scaledHeight, cropBounds, noTransformNeeded) =
|
||||||
if (transformedBitmap == originalFrame)
|
CalculateDimensions(originalWidth, originalHeight, decoder.DpiX, decoder.DpiY);
|
||||||
|
|
||||||
|
var (outputWidth, outputHeight) = noTransformNeeded
|
||||||
|
? (originalWidth, originalHeight)
|
||||||
|
: cropBounds.HasValue
|
||||||
|
? ((int)cropBounds.Value.Width, (int)cropBounds.Value.Height)
|
||||||
|
: ((int)scaledWidth, (int)scaledHeight);
|
||||||
|
|
||||||
|
path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
|
||||||
|
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
|
||||||
|
|
||||||
|
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.ReadWrite))
|
||||||
{
|
{
|
||||||
encoder.Frames.Add(originalFrame);
|
var winrtOutputStream = outputStream.AsRandomAccessStream();
|
||||||
|
await EncodeToStreamAsync(
|
||||||
|
decoder,
|
||||||
|
winrtInputStream,
|
||||||
|
winrtOutputStream,
|
||||||
|
encoderGuid,
|
||||||
|
async (encoder, isTranscode) =>
|
||||||
|
{
|
||||||
|
if (isTranscode)
|
||||||
|
{
|
||||||
|
if (!noTransformNeeded)
|
||||||
|
{
|
||||||
|
encoder.BitmapTransform.ScaledWidth = scaledWidth;
|
||||||
|
encoder.BitmapTransform.ScaledHeight = scaledHeight;
|
||||||
|
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
|
||||||
|
|
||||||
|
if (cropBounds.HasValue)
|
||||||
|
{
|
||||||
|
encoder.BitmapTransform.Bounds = cropBounds.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await EncodeFramesAsync(
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
cropBounds,
|
||||||
|
noTransformNeeded,
|
||||||
|
originalWidth,
|
||||||
|
originalHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
BitmapMetadata originalMetadata = (BitmapMetadata)originalFrame.Metadata;
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Debug.WriteLine($"### Processing metadata of file {_file}");
|
|
||||||
originalMetadata.PrintsAllMetadataToDebugOutput();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var metadata = GetValidMetadata(originalMetadata, transformedBitmap, containerFormat);
|
|
||||||
|
|
||||||
if (_settings.RemoveMetadata && metadata != null)
|
|
||||||
{
|
|
||||||
// strip any metadata that doesn't affect rendering
|
|
||||||
var newMetadata = new BitmapMetadata(metadata.Format);
|
|
||||||
|
|
||||||
metadata.CopyMetadataPropertyTo(newMetadata, "System.Photo.Orientation");
|
|
||||||
metadata.CopyMetadataPropertyTo(newMetadata, "System.Image.ColorSpace");
|
|
||||||
|
|
||||||
metadata = newMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
var frame = CreateBitmapFrame(transformedBitmap, metadata);
|
|
||||||
|
|
||||||
encoder.Frames.Add(frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path = GetDestinationPath(encoder);
|
|
||||||
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
|
|
||||||
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
|
|
||||||
{
|
|
||||||
encoder.Save(outputStream);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,54 +153,271 @@ namespace ImageResizer.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BitmapEncoder CreateEncoder(Guid containerFormat)
|
private async Task<string> ExecuteAiAsync(BitmapDecoder decoder, IRandomAccessStream winrtInputStream, Guid encoderGuid)
|
||||||
{
|
{
|
||||||
var createdEncoder = BitmapEncoder.Create(containerFormat);
|
try
|
||||||
if (!createdEncoder.CanEncode())
|
|
||||||
{
|
{
|
||||||
createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
|
using var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
|
||||||
}
|
BitmapPixelFormat.Bgra8,
|
||||||
|
BitmapAlphaMode.Premultiplied);
|
||||||
|
|
||||||
ConfigureEncoder(createdEncoder);
|
using var aiResult = _aiSuperResolutionService.ApplySuperResolution(
|
||||||
|
softwareBitmap,
|
||||||
|
_settings.AiSize.Scale,
|
||||||
|
_file);
|
||||||
|
|
||||||
return createdEncoder;
|
if (aiResult == null)
|
||||||
|
|
||||||
void ConfigureEncoder(BitmapEncoder encoder)
|
|
||||||
{
|
|
||||||
switch (encoder)
|
|
||||||
{
|
{
|
||||||
case JpegBitmapEncoder jpegEncoder:
|
throw new InvalidOperationException(ResourceLoaderInstance.GetString("Error_AiConversionFailed"));
|
||||||
jpegEncoder.QualityLevel = MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PngBitmapEncoder pngBitmapEncoder:
|
|
||||||
pngBitmapEncoder.Interlace = _settings.PngInterlaceOption;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TiffBitmapEncoder tiffEncoder:
|
|
||||||
tiffEncoder.Compression = _settings.TiffCompressOption;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var outputWidth = aiResult.PixelWidth;
|
||||||
|
var outputHeight = aiResult.PixelHeight;
|
||||||
|
|
||||||
|
var path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
|
||||||
|
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
|
||||||
|
|
||||||
|
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.ReadWrite))
|
||||||
|
{
|
||||||
|
var winrtOutputStream = outputStream.AsRandomAccessStream();
|
||||||
|
await EncodeToStreamAsync(
|
||||||
|
decoder,
|
||||||
|
winrtInputStream,
|
||||||
|
winrtOutputStream,
|
||||||
|
encoderGuid,
|
||||||
|
(encoder, _) =>
|
||||||
|
{
|
||||||
|
encoder.SetSoftwareBitmap(aiResult);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not InvalidOperationException)
|
||||||
|
{
|
||||||
|
var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
|
||||||
|
throw new InvalidOperationException(errorMessage, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BitmapSource Transform(BitmapSource source)
|
private async Task EncodeToStreamAsync(
|
||||||
|
BitmapDecoder decoder,
|
||||||
|
IRandomAccessStream inputStream,
|
||||||
|
IRandomAccessStream outputStream,
|
||||||
|
Guid encoderGuid,
|
||||||
|
Func<BitmapEncoder, bool, Task> writeContent)
|
||||||
{
|
{
|
||||||
if (_settings.SelectedSize is AiSize)
|
var decoderEncoderId = CodecHelper.GetEncoderIdForDecoder(decoder);
|
||||||
|
bool canTranscode = !_settings.RemoveMetadata
|
||||||
|
&& decoderEncoderId.HasValue
|
||||||
|
&& decoderEncoderId.Value == encoderGuid;
|
||||||
|
|
||||||
|
if (canTranscode)
|
||||||
{
|
{
|
||||||
return TransformWithAi(source);
|
await TranscodeAsync(decoder, inputStream, outputStream, writeContent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await FreshEncodeAsync(decoder, outputStream, encoderGuid, writeContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transcode path: re-encodes pixels via BitmapTransform while preserving all metadata.
|
||||||
|
/// The <paramref name="writeContent"/> callback receives isTranscode=true and should
|
||||||
|
/// configure <see cref="BitmapEncoder.BitmapTransform"/> properties only.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task TranscodeAsync(
|
||||||
|
BitmapDecoder decoder,
|
||||||
|
IRandomAccessStream inputStream,
|
||||||
|
IRandomAccessStream outputStream,
|
||||||
|
Func<BitmapEncoder, bool, Task> writeContent)
|
||||||
|
{
|
||||||
|
inputStream.Seek(0);
|
||||||
|
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
|
||||||
|
await writeContent(encoder, true);
|
||||||
|
|
||||||
|
// Safety net: some JPEG files with large/unusual metadata blocks (e.g. 54 KB
|
||||||
|
// embedded thumbnails) lose EXIF properties during transcode — the WPF equivalent
|
||||||
|
// threw InvalidOperationException on encoder.Metadata = decoder.Metadata for these.
|
||||||
|
// Re-set known critical properties to ensure they survive.
|
||||||
|
await CopyKnownMetadataAsync(decoder, encoder);
|
||||||
|
|
||||||
|
await encoder.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fresh encoder path: creates a blank encoder and manually writes pixel data.
|
||||||
|
/// Used when metadata must be stripped (RemoveMetadata) or format doesn't match (ICO→PNG).
|
||||||
|
/// The <paramref name="writeContent"/> callback receives isTranscode=false and should
|
||||||
|
/// call <see cref="EncodeFramesAsync"/> or <see cref="BitmapEncoder.SetSoftwareBitmap"/>.
|
||||||
|
/// </summary>
|
||||||
|
private async Task FreshEncodeAsync(
|
||||||
|
BitmapDecoder decoder,
|
||||||
|
IRandomAccessStream outputStream,
|
||||||
|
Guid encoderGuid,
|
||||||
|
Func<BitmapEncoder, bool, Task> writeContent)
|
||||||
|
{
|
||||||
|
// Read rendering-critical metadata before encoding so we can restore it on
|
||||||
|
// the blank encoder. Only needed for RemoveMetadata; format-mismatch files
|
||||||
|
// (e.g. ICO) rarely carry meaningful EXIF data.
|
||||||
|
BitmapPropertySet renderingMetadata = null;
|
||||||
|
if (_settings.RemoveMetadata)
|
||||||
|
{
|
||||||
|
renderingMetadata = await ReadMetadataAsync(decoder, RenderingMetadataProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
int originalWidth = source.PixelWidth;
|
var encoder = await CreateFreshEncoderAsync(encoderGuid, outputStream);
|
||||||
int originalHeight = source.PixelHeight;
|
await writeContent(encoder, false);
|
||||||
|
|
||||||
|
if (renderingMetadata != null)
|
||||||
|
{
|
||||||
|
await WriteMetadataAsync(encoder, renderingMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
await encoder.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes each frame, applies the transform, and writes pixel data to the encoder.
|
||||||
|
/// Uses GetPixelDataAsync + SetPixelData for explicit pixel format control — the
|
||||||
|
/// SetSoftwareBitmap API can fail with ArgumentException for some decoder outputs.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task EncodeFramesAsync(
|
||||||
|
BitmapEncoder encoder,
|
||||||
|
BitmapDecoder decoder,
|
||||||
|
uint scaledWidth,
|
||||||
|
uint scaledHeight,
|
||||||
|
BitmapBounds? cropBounds,
|
||||||
|
bool noTransformNeeded,
|
||||||
|
int originalWidth,
|
||||||
|
int originalHeight)
|
||||||
|
{
|
||||||
|
var transform = new BitmapTransform();
|
||||||
|
if (!noTransformNeeded)
|
||||||
|
{
|
||||||
|
transform.ScaledWidth = scaledWidth;
|
||||||
|
transform.ScaledHeight = scaledHeight;
|
||||||
|
transform.InterpolationMode = BitmapInterpolationMode.Fant;
|
||||||
|
|
||||||
|
if (cropBounds.HasValue)
|
||||||
|
{
|
||||||
|
transform.Bounds = cropBounds.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform.ScaledWidth = (uint)originalWidth;
|
||||||
|
transform.ScaledHeight = (uint)originalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint outWidth = cropBounds?.Width ?? (noTransformNeeded ? (uint)originalWidth : scaledWidth);
|
||||||
|
uint outHeight = cropBounds?.Height ?? (noTransformNeeded ? (uint)originalHeight : scaledHeight);
|
||||||
|
|
||||||
|
for (uint i = 0; i < decoder.FrameCount; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
await encoder.GoToNextFrameAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = await decoder.GetFrameAsync(i);
|
||||||
|
var pixelData = await frame.GetPixelDataAsync(
|
||||||
|
BitmapPixelFormat.Bgra8,
|
||||||
|
BitmapAlphaMode.Premultiplied,
|
||||||
|
transform,
|
||||||
|
ExifOrientationMode.IgnoreExifOrientation,
|
||||||
|
ColorManagementMode.DoNotColorManage);
|
||||||
|
|
||||||
|
encoder.SetPixelData(
|
||||||
|
BitmapPixelFormat.Bgra8,
|
||||||
|
BitmapAlphaMode.Premultiplied,
|
||||||
|
outWidth,
|
||||||
|
outHeight,
|
||||||
|
frame.DpiX,
|
||||||
|
frame.DpiY,
|
||||||
|
pixelData.DetachPixelData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] KnownMetadataProperties =
|
||||||
|
[
|
||||||
|
"System.Photo.DateTaken",
|
||||||
|
"System.Photo.CameraModel",
|
||||||
|
"System.Photo.CameraManufacturer",
|
||||||
|
"System.Photo.Orientation",
|
||||||
|
"System.Image.ColorSpace",
|
||||||
|
"System.Comment",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort read of metadata properties from the decoder.
|
||||||
|
/// Returns null if the format doesn't support metadata (e.g. BMP).
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<BitmapPropertySet> ReadMetadataAsync(BitmapDecoder decoder, string[] propertyNames)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var props = await decoder.BitmapProperties.GetPropertiesAsync(propertyNames);
|
||||||
|
if (props.Count > 0)
|
||||||
|
{
|
||||||
|
var result = new BitmapPropertySet();
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
result[prop.Key] = prop.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Some formats (e.g. BMP) don't support property queries.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort write of metadata properties to the encoder.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task WriteMetadataAsync(BitmapEncoder encoder, BitmapPropertySet metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null || metadata.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await encoder.BitmapProperties.SetPropertiesAsync(metadata);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Some encoders don't support these properties (e.g. BMP).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safety net for the transcode path: re-sets known EXIF properties that
|
||||||
|
/// CreateForTranscodingAsync may silently drop for files with large or
|
||||||
|
/// unusual metadata blocks (see TestMetadataIssue2447.jpg).
|
||||||
|
/// </summary>
|
||||||
|
private static async Task CopyKnownMetadataAsync(BitmapDecoder decoder, BitmapEncoder encoder)
|
||||||
|
{
|
||||||
|
var metadata = await ReadMetadataAsync(decoder, KnownMetadataProperties);
|
||||||
|
await WriteMetadataAsync(encoder, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (uint ScaledWidth, uint ScaledHeight, BitmapBounds? CropBounds, bool NoTransformNeeded) CalculateDimensions(
|
||||||
|
int originalWidth, int originalHeight, double dpiX, double dpiY)
|
||||||
|
{
|
||||||
// Convert from the chosen size unit to pixels, if necessary.
|
// Convert from the chosen size unit to pixels, if necessary.
|
||||||
double width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
|
double width = _settings.SelectedSize.GetPixelWidth(originalWidth, dpiX);
|
||||||
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.DpiY);
|
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, dpiY);
|
||||||
|
|
||||||
// Swap target width/height dimensions if orientation correction is required.
|
// Swap target width/height dimensions if orientation correction is required.
|
||||||
// Ensures that we don't try to fit a landscape image into a portrait box by
|
|
||||||
// distorting it, unless specific Auto/Percent rules are applied.
|
|
||||||
bool canSwapDimensions = _settings.IgnoreOrientation &&
|
bool canSwapDimensions = _settings.IgnoreOrientation &&
|
||||||
!_settings.SelectedSize.HasAuto &&
|
!_settings.SelectedSize.HasAuto &&
|
||||||
_settings.SelectedSize.Unit != ResizeUnit.Percent;
|
_settings.SelectedSize.Unit != ResizeUnit.Percent;
|
||||||
@@ -214,15 +443,11 @@ namespace ImageResizer.Models
|
|||||||
// Normalize scales based on the chosen Fit/Fill mode.
|
// Normalize scales based on the chosen Fit/Fill mode.
|
||||||
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
|
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
|
||||||
{
|
{
|
||||||
// Fit: use the smaller scale to ensure the image fits within the target.
|
|
||||||
scaleX = Math.Min(scaleX, scaleY);
|
scaleX = Math.Min(scaleX, scaleY);
|
||||||
scaleY = scaleX;
|
scaleY = scaleX;
|
||||||
}
|
}
|
||||||
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
|
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
|
||||||
{
|
{
|
||||||
// Fill: use the larger scale to ensure the target area is fully covered.
|
|
||||||
// This often results in one dimension overflowing, which is handled by
|
|
||||||
// cropping later.
|
|
||||||
scaleX = Math.Max(scaleX, scaleY);
|
scaleX = Math.Max(scaleX, scaleY);
|
||||||
scaleY = scaleX;
|
scaleY = scaleX;
|
||||||
}
|
}
|
||||||
@@ -230,177 +455,107 @@ namespace ImageResizer.Models
|
|||||||
// Handle Shrink Only mode.
|
// Handle Shrink Only mode.
|
||||||
if (_settings.ShrinkOnly && _settings.SelectedSize.Unit != ResizeUnit.Percent)
|
if (_settings.ShrinkOnly && _settings.SelectedSize.Unit != ResizeUnit.Percent)
|
||||||
{
|
{
|
||||||
// Shrink Only mode should never return an image larger than the original.
|
|
||||||
if (scaleX > 1 || scaleY > 1)
|
if (scaleX > 1 || scaleY > 1)
|
||||||
{
|
{
|
||||||
return source;
|
return ((uint)originalWidth, (uint)originalHeight, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow for crop-only when in Fill mode.
|
|
||||||
// At this point, the scale is <= 1.0. In Fill mode, it is possible for
|
|
||||||
// the scale to be 1.0 (no resize needed) while the target dimensions are
|
|
||||||
// smaller than the originals, requiring a crop.
|
|
||||||
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
|
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
|
||||||
(originalWidth > width || originalHeight > height);
|
(originalWidth > width || originalHeight > height);
|
||||||
|
|
||||||
// If the scale is exactly 1.0 and a crop isn't required, we return the
|
|
||||||
// original image to prevent a re-encode.
|
|
||||||
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
|
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
|
||||||
{
|
{
|
||||||
return source;
|
return ((uint)originalWidth, (uint)originalHeight, null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the scaling.
|
// Calculate scaled dimensions
|
||||||
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
|
uint scaledWidth = (uint)Math.Max(1, (int)Math.Round(originalWidth * scaleX));
|
||||||
|
uint scaledHeight = (uint)Math.Max(1, (int)Math.Round(originalHeight * scaleY));
|
||||||
|
|
||||||
// Apply the centered crop for Fill mode, if necessary. Applies when Fill
|
// Apply the centered crop for Fill mode, if necessary.
|
||||||
// mode caused the scaled image to exceed the target dimensions.
|
|
||||||
if (_settings.SelectedSize.Fit == ResizeFit.Fill
|
if (_settings.SelectedSize.Fit == ResizeFit.Fill
|
||||||
&& (scaledBitmap.PixelWidth > width
|
&& (scaledWidth > (uint)width || scaledHeight > (uint)height))
|
||||||
|| scaledBitmap.PixelHeight > height))
|
|
||||||
{
|
{
|
||||||
int x = (int)(((originalWidth * scaleX) - width) / 2);
|
uint cropX = (uint)(((originalWidth * scaleX) - width) / 2);
|
||||||
int y = (int)(((originalHeight * scaleY) - height) / 2);
|
uint cropY = (uint)(((originalHeight * scaleY) - height) / 2);
|
||||||
|
|
||||||
return new CroppedBitmap(scaledBitmap, new Int32Rect(x, y, (int)width, (int)height));
|
var cropBounds = new BitmapBounds
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
X = cropX,
|
||||||
}
|
Y = cropY,
|
||||||
|
Width = (uint)width,
|
||||||
|
Height = (uint)height,
|
||||||
|
};
|
||||||
|
|
||||||
return result;
|
return (scaledWidth, scaledHeight, cropBounds, false);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (scaledWidth, scaledHeight, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private async Task<BitmapEncoder> CreateFreshEncoderAsync(Guid encoderGuid, IRandomAccessStream outputStream)
|
||||||
/// 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.
|
|
||||||
/// We return null if we were not able to get hold of valid metadata.
|
|
||||||
/// </summary>
|
|
||||||
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
|
|
||||||
{
|
{
|
||||||
if (originalMetadata == null)
|
var propertySet = GetEncoderPropertySet(encoderGuid);
|
||||||
|
return propertySet != null
|
||||||
|
? await BitmapEncoder.CreateAsync(encoderGuid, outputStream, propertySet)
|
||||||
|
: await BitmapEncoder.CreateAsync(encoderGuid, outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetJpegQualityFraction()
|
||||||
|
=> (float)Math.Clamp(_settings.JpegQualityLevel, 1, 100) / 100f;
|
||||||
|
|
||||||
|
private BitmapPropertySet GetEncoderPropertySet(Guid encoderGuid)
|
||||||
|
{
|
||||||
|
if (encoderGuid == BitmapEncoder.JpegEncoderId)
|
||||||
{
|
{
|
||||||
return null;
|
return new BitmapPropertySet
|
||||||
|
{
|
||||||
|
{ "ImageQuality", new BitmapTypedValue(GetJpegQualityFraction(), PropertyType.Single) },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the original metadata is valid
|
if (encoderGuid == BitmapEncoder.TiffEncoderId)
|
||||||
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
|
|
||||||
if (EnsureFrameIsValid(frameWithOriginalMetadata))
|
|
||||||
{
|
{
|
||||||
return originalMetadata;
|
var compressionMethod = MapTiffCompression(_settings.TiffCompressOption);
|
||||||
|
if (compressionMethod.HasValue)
|
||||||
|
{
|
||||||
|
return new BitmapPropertySet
|
||||||
|
{
|
||||||
|
{ "TiffCompressionMethod", new BitmapTypedValue(compressionMethod.Value, PropertyType.UInt8) },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original metadata was invalid. We try to rebuild the metadata object from the scratch and discard invalid metadata fields
|
|
||||||
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
|
|
||||||
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
|
|
||||||
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
|
|
||||||
{
|
|
||||||
return recreatedMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seems like we have an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// The safest way to check if the metadata object is valid is to call Save() on the encoder.
|
|
||||||
// I tried other ways to check if metadata is valid (like calling Clone() on the metadata object) but this was not reliable resulting in a few github issues.
|
|
||||||
bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var encoder = CreateEncoder(containerFormat);
|
|
||||||
encoder.Frames.Add(frameToBeChecked);
|
|
||||||
using (var testStream = new MemoryStream())
|
|
||||||
{
|
|
||||||
encoder.Save(testStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static byte? MapTiffCompression(TiffCompressOption option)
|
||||||
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
|
|
||||||
/// </summary>
|
|
||||||
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
|
|
||||||
{
|
{
|
||||||
try
|
return option switch
|
||||||
{
|
{
|
||||||
var metadata = new BitmapMetadata(originalMetadata.Format);
|
TiffCompressOption.None => 1,
|
||||||
var listOfMetadata = originalMetadata.GetListOfMetadata();
|
TiffCompressOption.Ccitt3 => 2,
|
||||||
foreach (var (metadataPath, value) in listOfMetadata)
|
TiffCompressOption.Ccitt4 => 3,
|
||||||
{
|
TiffCompressOption.Lzw => 4,
|
||||||
if (value is BitmapMetadata bitmapMetadata)
|
TiffCompressOption.Rle => 5,
|
||||||
{
|
TiffCompressOption.Zip => 6,
|
||||||
var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
|
_ => null, // Default: let the encoder decide
|
||||||
metadata.SetQuerySafe(metadataPath, innerMetadata);
|
};
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
metadata.SetQuerySafe(metadataPath, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine(ex);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BitmapFrame CreateBitmapFrame(BitmapSource transformedBitmap, BitmapMetadata metadata)
|
private string GetDestinationPath(Guid encoderGuid, int outputPixelWidth, int outputPixelHeight)
|
||||||
{
|
|
||||||
return BitmapFrame.Create(
|
|
||||||
transformedBitmap,
|
|
||||||
thumbnail: null, /* should be null, see #15413 */
|
|
||||||
metadata,
|
|
||||||
colorContexts: null /* should be null, see #14866 */ );
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDestinationPath(BitmapEncoder encoder)
|
|
||||||
{
|
{
|
||||||
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
|
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
|
||||||
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
||||||
|
|
||||||
var supportedExtensions = encoder.CodecInfo.FileExtensions.Split(',');
|
var supportedExtensions = CodecHelper.GetSupportedExtensions(encoderGuid);
|
||||||
var extension = _fileSystem.Path.GetExtension(_file);
|
var extension = _fileSystem.Path.GetExtension(_file);
|
||||||
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
extension = supportedExtensions.FirstOrDefault();
|
extension = CodecHelper.GetDefaultExtension(encoderGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove directory characters from the size's name.
|
|
||||||
// For AI Size, use the scale display (e.g., "2×") instead of the full name
|
|
||||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||||
? aiSize.ScaleDisplay
|
? aiSize.ScaleDisplay
|
||||||
: _settings.SelectedSize.Name;
|
: _settings.SelectedSize.Name;
|
||||||
@@ -408,9 +563,8 @@ namespace ImageResizer.Models
|
|||||||
.Replace('\\', '_')
|
.Replace('\\', '_')
|
||||||
.Replace('/', '_');
|
.Replace('/', '_');
|
||||||
|
|
||||||
// Using CurrentCulture since this is user facing
|
var selectedWidth = _settings.SelectedSize is AiSize ? outputPixelWidth : _settings.SelectedSize.Width;
|
||||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
var selectedHeight = _settings.SelectedSize is AiSize ? outputPixelHeight : _settings.SelectedSize.Height;
|
||||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
|
||||||
var fileName = string.Format(
|
var fileName = string.Format(
|
||||||
CultureInfo.CurrentCulture,
|
CultureInfo.CurrentCulture,
|
||||||
_settings.FileNameFormat,
|
_settings.FileNameFormat,
|
||||||
@@ -418,10 +572,9 @@ namespace ImageResizer.Models
|
|||||||
sizeNameSanitized,
|
sizeNameSanitized,
|
||||||
selectedWidth,
|
selectedWidth,
|
||||||
selectedHeight,
|
selectedHeight,
|
||||||
encoder.Frames[0].PixelWidth,
|
outputPixelWidth,
|
||||||
encoder.Frames[0].PixelHeight);
|
outputPixelHeight);
|
||||||
|
|
||||||
// Remove invalid characters from the final file name.
|
|
||||||
fileName = fileName
|
fileName = fileName
|
||||||
.Replace(':', '_')
|
.Replace(':', '_')
|
||||||
.Replace('*', '_')
|
.Replace('*', '_')
|
||||||
@@ -431,7 +584,6 @@ namespace ImageResizer.Models
|
|||||||
.Replace('>', '_')
|
.Replace('>', '_')
|
||||||
.Replace('|', '_');
|
.Replace('|', '_');
|
||||||
|
|
||||||
// Avoid creating not recommended filenames
|
|
||||||
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
|
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
|
||||||
{
|
{
|
||||||
fileName = fileName + "_";
|
fileName = fileName + "_";
|
||||||
|
|||||||
@@ -1,36 +1,52 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ImageResizer.Helpers;
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Properties;
|
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
public class ResizeSize : Observable, IHasId
|
public partial class ResizeSize : ObservableObject, IHasId
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> _tokens = new Dictionary<string, string>
|
private static readonly Dictionary<string, string> _tokenKeys = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["$small$"] = Resources.Small,
|
["$small$"] = "Small",
|
||||||
["$medium$"] = Resources.Medium,
|
["$medium$"] = "Medium",
|
||||||
["$large$"] = Resources.Large,
|
["$large$"] = "Large",
|
||||||
["$phone$"] = Resources.Phone,
|
["$phone$"] = "Phone",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("Id")]
|
||||||
private int _id;
|
private int _id;
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("fit")]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||||
private ResizeFit _fit = ResizeFit.Fit;
|
private ResizeFit _fit = ResizeFit.Fit;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("width")]
|
||||||
private double _width;
|
private double _width;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("height")]
|
||||||
private double _height;
|
private double _height;
|
||||||
private bool _showHeight = true;
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[JsonPropertyName("unit")]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||||
private ResizeUnit _unit = ResizeUnit.Pixel;
|
private ResizeUnit _unit = ResizeUnit.Pixel;
|
||||||
|
|
||||||
public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
|
public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
|
||||||
@@ -47,73 +63,18 @@ namespace ImageResizer.Models
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("Id")]
|
|
||||||
public int Id
|
|
||||||
{
|
|
||||||
get => _id;
|
|
||||||
set => Set(ref _id, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public virtual string Name
|
public virtual string Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
set => Set(ref _name, ReplaceTokens(value));
|
set => SetProperty(ref _name, ReplaceTokens(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("fit")]
|
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||||
public ResizeFit Fit
|
|
||||||
{
|
|
||||||
get => _fit;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
var previousFit = _fit;
|
|
||||||
Set(ref _fit, value);
|
|
||||||
if (!Equals(previousFit, value))
|
|
||||||
{
|
|
||||||
UpdateShowHeight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("width")]
|
|
||||||
public double Width
|
|
||||||
{
|
|
||||||
get => _width;
|
|
||||||
set => Set(ref _width, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("height")]
|
|
||||||
public double Height
|
|
||||||
{
|
|
||||||
get => _height;
|
|
||||||
set => Set(ref _height, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowHeight
|
|
||||||
{
|
|
||||||
get => _showHeight;
|
|
||||||
set => Set(ref _showHeight, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAuto
|
public bool HasAuto
|
||||||
=> Width == 0 || Height == 0 || double.IsNaN(Width) || double.IsNaN(Height);
|
=> Width == 0 || Height == 0 || double.IsNaN(Width) || double.IsNaN(Height);
|
||||||
|
|
||||||
[JsonPropertyName("unit")]
|
|
||||||
public ResizeUnit Unit
|
|
||||||
{
|
|
||||||
get => _unit;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
var previousUnit = _unit;
|
|
||||||
Set(ref _unit, value);
|
|
||||||
if (!Equals(previousUnit, value))
|
|
||||||
{
|
|
||||||
UpdateShowHeight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public double GetPixelWidth(int originalWidth, double dpi)
|
public double GetPixelWidth(int originalWidth, double dpi)
|
||||||
=> ConvertToPixels(Width, Unit, originalWidth, dpi);
|
=> ConvertToPixels(Width, Unit, originalWidth, dpi);
|
||||||
|
|
||||||
@@ -127,15 +88,10 @@ namespace ImageResizer.Models
|
|||||||
dpi);
|
dpi);
|
||||||
|
|
||||||
private static string ReplaceTokens(string text)
|
private static string ReplaceTokens(string text)
|
||||||
=> (text != null && _tokens.TryGetValue(text, out var result))
|
=> text != null && _tokenKeys.TryGetValue(text, out var key)
|
||||||
? result
|
? ResourceLoaderInstance.GetString(key)
|
||||||
: text;
|
: text;
|
||||||
|
|
||||||
private void UpdateShowHeight()
|
|
||||||
{
|
|
||||||
ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double ConvertToPixels(double value, ResizeUnit unit, int originalValue, double dpi)
|
private double ConvertToPixels(double value, ResizeUnit unit, int originalValue, double dpi)
|
||||||
{
|
{
|
||||||
if (value == 0 || double.IsNaN(value))
|
if (value == 0 || double.IsNaN(value))
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
namespace ImageResizer.Models
|
namespace ImageResizer.Models
|
||||||
{
|
{
|
||||||
|
|||||||
27
src/modules/imageresizer/ui/Program.cs
Normal file
27
src/modules/imageresizer/ui/Program.cs
Normal file
@@ -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;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace ImageResizer
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||||
|
Application.Start((p) =>
|
||||||
|
{
|
||||||
|
var context = new DispatcherQueueSynchronizationContext(
|
||||||
|
DispatcherQueue.GetForCurrentThread());
|
||||||
|
SynchronizationContext.SetSynchronizationContext(context);
|
||||||
|
_ = new App();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("ImageResizer.Test")]
|
|
||||||
1152
src/modules/imageresizer/ui/Properties/Resources.Designer.cs
generated
1152
src/modules/imageresizer/ui/Properties/Resources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
75
src/modules/imageresizer/ui/Properties/Resources.cs
Normal file
75
src/modules/imageresizer/ui/Properties/Resources.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 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 ImageResizer.Helpers;
|
||||||
|
|
||||||
|
namespace ImageResizer.Properties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resource accessor class for compatibility with CLI code and tests.
|
||||||
|
/// Wraps ResourceLoader for resource string access.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Resources
|
||||||
|
{
|
||||||
|
// Size names (used by tests and ResizeSize token replacement)
|
||||||
|
public static string Small => ResourceLoaderInstance.GetString("Small");
|
||||||
|
|
||||||
|
public static string Medium => ResourceLoaderInstance.GetString("Medium");
|
||||||
|
|
||||||
|
public static string Large => ResourceLoaderInstance.GetString("Large");
|
||||||
|
|
||||||
|
public static string Phone => ResourceLoaderInstance.GetString("Phone");
|
||||||
|
|
||||||
|
// Input page resources
|
||||||
|
public static string Input_Custom => ResourceLoaderInstance.GetString("Input_Custom");
|
||||||
|
|
||||||
|
// Validation messages
|
||||||
|
public static string ValueMustBeBetween => ResourceLoaderInstance.GetString("ValueMustBeBetween");
|
||||||
|
|
||||||
|
// CLI options
|
||||||
|
public static string CLI_Option_Destination => ResourceLoaderInstance.GetString("CLI_Option_Destination");
|
||||||
|
|
||||||
|
public static string CLI_Option_FileName => ResourceLoaderInstance.GetString("CLI_Option_FileName");
|
||||||
|
|
||||||
|
public static string CLI_Option_Files => ResourceLoaderInstance.GetString("CLI_Option_Files");
|
||||||
|
|
||||||
|
public static string CLI_Option_Fit => ResourceLoaderInstance.GetString("CLI_Option_Fit");
|
||||||
|
|
||||||
|
public static string CLI_Option_Height => ResourceLoaderInstance.GetString("CLI_Option_Height");
|
||||||
|
|
||||||
|
public static string CLI_Option_Help => ResourceLoaderInstance.GetString("CLI_Option_Help");
|
||||||
|
|
||||||
|
public static string CLI_Option_IgnoreOrientation => ResourceLoaderInstance.GetString("CLI_Option_IgnoreOrientation");
|
||||||
|
|
||||||
|
public static string CLI_Option_KeepDateModified => ResourceLoaderInstance.GetString("CLI_Option_KeepDateModified");
|
||||||
|
|
||||||
|
public static string CLI_Option_Quality => ResourceLoaderInstance.GetString("CLI_Option_Quality");
|
||||||
|
|
||||||
|
public static string CLI_Option_Replace => ResourceLoaderInstance.GetString("CLI_Option_Replace");
|
||||||
|
|
||||||
|
public static string CLI_Option_ShowConfig => ResourceLoaderInstance.GetString("CLI_Option_ShowConfig");
|
||||||
|
|
||||||
|
public static string CLI_Option_ShrinkOnly => ResourceLoaderInstance.GetString("CLI_Option_ShrinkOnly");
|
||||||
|
|
||||||
|
public static string CLI_Option_RemoveMetadata => ResourceLoaderInstance.GetString("CLI_Option_RemoveMetadata");
|
||||||
|
|
||||||
|
public static string CLI_Option_Size => ResourceLoaderInstance.GetString("CLI_Option_Size");
|
||||||
|
|
||||||
|
public static string CLI_Option_Unit => ResourceLoaderInstance.GetString("CLI_Option_Unit");
|
||||||
|
|
||||||
|
public static string CLI_Option_Width => ResourceLoaderInstance.GetString("CLI_Option_Width");
|
||||||
|
|
||||||
|
public static string CLI_ProcessingFiles => ResourceLoaderInstance.GetString("CLI_ProcessingFiles");
|
||||||
|
|
||||||
|
public static string CLI_ProgressFormat => ResourceLoaderInstance.GetString("CLI_ProgressFormat");
|
||||||
|
|
||||||
|
public static string CLI_CompletedWithErrors => ResourceLoaderInstance.GetString("CLI_CompletedWithErrors");
|
||||||
|
|
||||||
|
public static string CLI_AllFilesProcessed => ResourceLoaderInstance.GetString("CLI_AllFilesProcessed");
|
||||||
|
|
||||||
|
public static string CLI_WarningInvalidSizeIndex => ResourceLoaderInstance.GetString("CLI_WarningInvalidSizeIndex");
|
||||||
|
|
||||||
|
public static string CLI_NoInputFiles => ResourceLoaderInstance.GetString("CLI_NoInputFiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
@@ -16,13 +16,10 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
using System.Text.Json.Serialization.Metadata;
|
||||||
using System.Threading;
|
using ImageResizer.Helpers;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
|
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Services;
|
|
||||||
using ImageResizer.ViewModels;
|
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
|
||||||
namespace ImageResizer.Properties
|
namespace ImageResizer.Properties
|
||||||
{
|
{
|
||||||
@@ -42,14 +39,21 @@ namespace ImageResizer.Properties
|
|||||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
|
// Cached UI thread DispatcherQueue for cross-thread property change notifications
|
||||||
|
private static DispatcherQueue _uiDispatcherQueue;
|
||||||
|
|
||||||
// Used to synchronize access to the settings.json file
|
private static CompositeFormat _valueMustBeBetween;
|
||||||
private static Mutex _jsonMutex = new Mutex();
|
|
||||||
|
private static CompositeFormat ValueMustBeBetween =>
|
||||||
|
_valueMustBeBetween ??= System.Text.CompositeFormat.Parse(ResourceLoaderInstance.GetString("ValueMustBeBetween"));
|
||||||
|
|
||||||
|
// Used to synchronize access to the settings.json file (in-process only)
|
||||||
|
private static readonly System.Threading.Lock _jsonSyncLock = new();
|
||||||
private static string _settingsPath = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "Image Resizer", "settings.json");
|
private static string _settingsPath = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "Image Resizer", "settings.json");
|
||||||
private string _fileNameFormat;
|
private string _fileNameFormat;
|
||||||
private bool _shrinkOnly;
|
private bool _shrinkOnly;
|
||||||
@@ -74,8 +78,8 @@ namespace ImageResizer.Properties
|
|||||||
IgnoreOrientation = true;
|
IgnoreOrientation = true;
|
||||||
RemoveMetadata = false;
|
RemoveMetadata = false;
|
||||||
JpegQualityLevel = 90;
|
JpegQualityLevel = 90;
|
||||||
PngInterlaceOption = System.Windows.Media.Imaging.PngInterlaceOption.Default;
|
PngInterlaceOption = Models.PngInterlaceOption.Default;
|
||||||
TiffCompressOption = System.Windows.Media.Imaging.TiffCompressOption.Default;
|
TiffCompressOption = Models.TiffCompressOption.Default;
|
||||||
FileName = "%1 (%2)";
|
FileName = "%1 (%2)";
|
||||||
Sizes = new ObservableCollection<ResizeSize>
|
Sizes = new ObservableCollection<ResizeSize>
|
||||||
{
|
{
|
||||||
@@ -87,32 +91,25 @@ namespace ImageResizer.Properties
|
|||||||
KeepDateModified = false;
|
KeepDateModified = false;
|
||||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||||
AiSize = new AiSize(2); // Initialize with default scale of 2
|
AiSize = new AiSize(2);
|
||||||
AllSizes = new AllSizesCollection(this);
|
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()
|
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
|
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||||
? Sizes.Count // CustomSize only
|
? Sizes.Count
|
||||||
: Sizes.Count + 1; // CustomSize + AiSize
|
: Sizes.Count + 1;
|
||||||
|
|
||||||
if (_selectedSizeIndex > maxIndex)
|
if (_selectedSizeIndex > maxIndex)
|
||||||
{
|
{
|
||||||
_selectedSizeIndex = 0; // Reset to first size
|
_selectedSizeIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||||
|
|
||||||
// Using OrdinalIgnoreCase since this is internal and used for comparison with symbols
|
|
||||||
public string FileNameFormat
|
public string FileNameFormat
|
||||||
=> _fileNameFormat
|
=> _fileNameFormat
|
||||||
?? (_fileNameFormat = FileName
|
?? (_fileNameFormat = FileName
|
||||||
@@ -144,7 +141,6 @@ namespace ImageResizer.Properties
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback to CustomSize when index is out of range or AI is not available
|
|
||||||
return CustomSize;
|
return CustomSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,13 +164,7 @@ namespace ImageResizer.Properties
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string IDataErrorInfo.Error
|
string IDataErrorInfo.Error => string.Empty;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string IDataErrorInfo.this[string columnName]
|
string IDataErrorInfo.this[string columnName]
|
||||||
{
|
{
|
||||||
@@ -187,7 +177,6 @@ namespace ImageResizer.Properties
|
|||||||
|
|
||||||
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
|
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
|
||||||
{
|
{
|
||||||
// Using CurrentCulture since this is user facing
|
|
||||||
return string.Format(CultureInfo.CurrentCulture, ValueMustBeBetween, 1, 100);
|
return string.Format(CultureInfo.CurrentCulture, ValueMustBeBetween, 1, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,26 +206,20 @@ namespace ImageResizer.Properties
|
|||||||
if (e.PropertyName == nameof(Models.CustomSize))
|
if (e.PropertyName == nameof(Models.CustomSize))
|
||||||
{
|
{
|
||||||
_customSize = settings.CustomSize;
|
_customSize = settings.CustomSize;
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
}
|
}
|
||||||
else if (e.PropertyName == nameof(Models.AiSize))
|
else if (e.PropertyName == nameof(Models.AiSize))
|
||||||
{
|
{
|
||||||
_aiSize = settings.AiSize;
|
_aiSize = settings.AiSize;
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
}
|
}
|
||||||
else if (e.PropertyName == nameof(Sizes))
|
else if (e.PropertyName == nameof(Sizes))
|
||||||
{
|
{
|
||||||
var oldSizes = _sizes;
|
var oldSizes = _sizes;
|
||||||
|
|
||||||
oldSizes.CollectionChanged -= HandleCollectionChanged;
|
oldSizes.CollectionChanged -= HandleCollectionChanged;
|
||||||
((INotifyPropertyChanged)oldSizes).PropertyChanged -= HandlePropertyChanged;
|
((INotifyPropertyChanged)oldSizes).PropertyChanged -= HandlePropertyChanged;
|
||||||
|
|
||||||
_sizes = settings.Sizes;
|
_sizes = settings.Sizes;
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
|
|
||||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||||
}
|
}
|
||||||
@@ -244,7 +227,6 @@ namespace ImageResizer.Properties
|
|||||||
}
|
}
|
||||||
|
|
||||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||||
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
@@ -291,7 +273,6 @@ namespace ImageResizer.Properties
|
|||||||
private class AllSizesEnumerator : IEnumerator<ResizeSize>
|
private class AllSizesEnumerator : IEnumerator<ResizeSize>
|
||||||
{
|
{
|
||||||
private readonly AllSizesCollection _list;
|
private readonly AllSizesCollection _list;
|
||||||
|
|
||||||
private int _index = -1;
|
private int _index = -1;
|
||||||
|
|
||||||
public AllSizesEnumerator(AllSizesCollection list)
|
public AllSizesEnumerator(AllSizesCollection list)
|
||||||
@@ -334,9 +315,13 @@ namespace ImageResizer.Properties
|
|||||||
get => _selectedSizeIndex;
|
get => _selectedSizeIndex;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (_selectedSizeIndex == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_selectedSizeIndex = value;
|
_selectedSizeIndex = value;
|
||||||
NotifyPropertyChanged();
|
NotifyPropertyChanged();
|
||||||
NotifyPropertyChanged(nameof(SelectedSize));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,15 +361,6 @@ namespace ImageResizer.Properties
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a value indicating whether resizing images removes any metadata that doesn't affect rendering.
|
|
||||||
/// Default is false.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Preserved Metadata:
|
|
||||||
/// System.Photo.Orientation,
|
|
||||||
/// System.Image.ColorSpace
|
|
||||||
/// </remarks>
|
|
||||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||||
[JsonPropertyName("imageresizer_removeMetadata")]
|
[JsonPropertyName("imageresizer_removeMetadata")]
|
||||||
public bool RemoveMetadata
|
public bool RemoveMetadata
|
||||||
@@ -505,6 +481,15 @@ namespace ImageResizer.Properties
|
|||||||
|
|
||||||
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the UI DispatcherQueue for cross-thread property change notifications.
|
||||||
|
/// Must be called from the UI thread during app startup.
|
||||||
|
/// </summary>
|
||||||
|
public static void InitializeDispatcher()
|
||||||
|
{
|
||||||
|
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
||||||
@@ -514,16 +499,23 @@ namespace ImageResizer.Properties
|
|||||||
|
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
_jsonMutex.WaitOne();
|
lock (_jsonSyncLock)
|
||||||
|
{
|
||||||
|
SaveCore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes current settings to disk. Must be called under <see cref="_jsonSyncLock"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void SaveCore()
|
||||||
|
{
|
||||||
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this }, _jsonSerializerOptions);
|
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this }, _jsonSerializerOptions);
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
IFileInfo file = _fileSystem.FileInfo.New(SettingsPath);
|
IFileInfo file = _fileSystem.FileInfo.New(SettingsPath);
|
||||||
file.Directory.Create();
|
file.Directory.Create();
|
||||||
|
|
||||||
// write string to file
|
|
||||||
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
|
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
|
||||||
_jsonMutex.ReleaseMutex();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reload()
|
public void Reload()
|
||||||
@@ -536,35 +528,46 @@ namespace ImageResizer.Properties
|
|||||||
_fileSystem.Directory.Move(oldSettingsDir, settingsDir);
|
_fileSystem.Directory.Move(oldSettingsDir, settingsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
_jsonMutex.WaitOne();
|
// Read and deserialize under lock; ReloadCore runs outside the lock
|
||||||
if (!_fileSystem.File.Exists(SettingsPath))
|
// because jsonSettings is an in-memory snapshot with no file I/O.
|
||||||
|
Settings jsonSettings;
|
||||||
|
lock (_jsonSyncLock)
|
||||||
{
|
{
|
||||||
_jsonMutex.ReleaseMutex();
|
if (!_fileSystem.File.Exists(SettingsPath))
|
||||||
Save();
|
{
|
||||||
return;
|
SaveCore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string jsonData = _fileSystem.File.ReadAllText(SettingsPath);
|
||||||
|
jsonSettings = new Settings();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonSettings = JsonSerializer.Deserialize<SettingsWrapper>(jsonData, _jsonSerializerOptions)?.Properties;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to parse settings JSON, using defaults: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string jsonData = _fileSystem.File.ReadAllText(SettingsPath);
|
// Apply deserialized snapshot to live properties on the UI thread.
|
||||||
var jsonSettings = new Settings();
|
if (_uiDispatcherQueue != null)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
jsonSettings = JsonSerializer.Deserialize<SettingsWrapper>(jsonData, _jsonSerializerOptions)?.Properties;
|
if (_uiDispatcherQueue.HasThreadAccess)
|
||||||
}
|
{
|
||||||
catch (JsonException)
|
ReloadCore(jsonSettings);
|
||||||
{
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
if (App.Current?.Dispatcher != null)
|
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
|
||||||
{
|
}
|
||||||
// Needs to be called on the App UI thread as the properties are bound to the UI.
|
|
||||||
App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// No UI context (unit tests or CLI mode) — call directly.
|
||||||
ReloadCore(jsonSettings);
|
ReloadCore(jsonSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
_jsonMutex.ReleaseMutex();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadCore(Settings jsonSettings)
|
private void ReloadCore(Settings jsonSettings)
|
||||||
@@ -580,20 +583,16 @@ namespace ImageResizer.Properties
|
|||||||
KeepDateModified = jsonSettings.KeepDateModified;
|
KeepDateModified = jsonSettings.KeepDateModified;
|
||||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||||
CustomSize = jsonSettings.CustomSize;
|
CustomSize = jsonSettings.CustomSize;
|
||||||
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
|
AiSize = jsonSettings.AiSize ?? new AiSize(2);
|
||||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||||
|
|
||||||
if (jsonSettings.Sizes.Count > 0)
|
if (jsonSettings.Sizes.Count > 0)
|
||||||
{
|
{
|
||||||
Sizes.Clear();
|
Sizes.Clear();
|
||||||
Sizes.AddRange(jsonSettings.Sizes);
|
Sizes.AddRange(jsonSettings.Sizes);
|
||||||
|
|
||||||
// Ensure Ids are unique and handle missing Ids
|
|
||||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate SelectedSizeIndex after Sizes collection has been updated
|
|
||||||
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
|
|
||||||
ValidateSelectedSizeIndex();
|
ValidateSelectedSizeIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Windows.Media.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
|
|
||||||
namespace ImageResizer.Services
|
namespace ImageResizer.Services
|
||||||
{
|
{
|
||||||
public interface IAISuperResolutionService : IDisposable
|
public interface IAISuperResolutionService : IDisposable
|
||||||
{
|
{
|
||||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Windows.Media.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
|
|
||||||
namespace ImageResizer.Services
|
namespace ImageResizer.Services
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,7 @@ namespace ImageResizer.Services
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
|
||||||
{
|
{
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Runtime.InteropServices.WindowsRuntime;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using ManagedCommon;
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using Microsoft.Windows.AI;
|
using Microsoft.Windows.AI;
|
||||||
using Microsoft.Windows.AI.Imaging;
|
using Microsoft.Windows.AI.Imaging;
|
||||||
using Windows.Graphics.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
@@ -47,8 +42,9 @@ namespace ImageResizer.Services
|
|||||||
|
|
||||||
return new WinAiSuperResolutionService(imageScaler);
|
return new WinAiSuperResolutionService(imageScaler);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logger.LogError($"Failed to create AI super resolution service: {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,39 +55,28 @@ namespace ImageResizer.Services
|
|||||||
{
|
{
|
||||||
return ImageScaler.GetReadyState();
|
return ImageScaler.GetReadyState();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// If we can't get the state, treat it as disabled by user
|
// If we can't get the state, treat it as disabled by user
|
||||||
// The caller should check if it's Ready or NotReady
|
Logger.LogWarning($"Failed to get AI model ready state: {ex.Message}");
|
||||||
return AIFeatureReadyState.DisabledByUser;
|
return AIFeatureReadyState.DisabledByUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync(IProgress<double> progress = null)
|
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var operation = ImageScaler.EnsureReadyAsync();
|
return await 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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logger.LogError($"Failed to ensure AI model ready: {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
|
||||||
{
|
{
|
||||||
if (source == null || _disposed)
|
if (source == null || _disposed)
|
||||||
{
|
{
|
||||||
@@ -102,19 +87,12 @@ namespace ImageResizer.Services
|
|||||||
// Currently not used by the ImageScaler API
|
// Currently not used by the ImageScaler API
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Convert WPF BitmapSource to WinRT SoftwareBitmap
|
|
||||||
var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
|
|
||||||
if (softwareBitmap == null)
|
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate target dimensions
|
// Calculate target dimensions
|
||||||
var newWidth = softwareBitmap.PixelWidth * scale;
|
var newWidth = source.PixelWidth * scale;
|
||||||
var newHeight = softwareBitmap.PixelHeight * scale;
|
var newHeight = source.PixelHeight * scale;
|
||||||
|
|
||||||
// Apply super resolution with thread-safe access
|
// Apply super resolution with thread-safe access
|
||||||
// _usageLock protects concurrent access from Parallel.ForEach threads
|
// _usageLock protects concurrent access from Parallel.ForEachAsync threads
|
||||||
SoftwareBitmap scaledBitmap;
|
SoftwareBitmap scaledBitmap;
|
||||||
lock (_usageLock)
|
lock (_usageLock)
|
||||||
{
|
{
|
||||||
@@ -123,120 +101,19 @@ namespace ImageResizer.Services
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
|
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(source, newWidth, newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scaledBitmap == null)
|
return scaledBitmap ?? source;
|
||||||
{
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert back to WPF BitmapSource
|
|
||||||
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Any error, return original image gracefully
|
// Any error, return original image gracefully
|
||||||
|
Logger.LogError($"AI super resolution failed for {filePath}: {ex.Message}");
|
||||||
return source;
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
@@ -117,18 +117,21 @@
|
|||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
<data name="AllFilesFilter" xml:space="preserve">
|
<data name="ImageResizer" xml:space="preserve">
|
||||||
<value>All Files</value>
|
<value>Image Resizer</value>
|
||||||
|
<comment>Product name, do not loc</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Cancel" xml:space="preserve">
|
<data name="Cancel.Text" xml:space="preserve">
|
||||||
<value>Cancel</value>
|
<value>Cancel</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Height" xml:space="preserve">
|
<data name="Height" xml:space="preserve">
|
||||||
<value>Height</value>
|
<value>Height</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ImageResizer" xml:space="preserve">
|
<data name="Width" xml:space="preserve">
|
||||||
<value>Image Resizer</value>
|
<value>Width</value>
|
||||||
<comment>Product name, do not loc</comment>
|
</data>
|
||||||
|
<data name="Unit" xml:space="preserve">
|
||||||
|
<value>Unit</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_Auto" xml:space="preserve">
|
<data name="Input_Auto" xml:space="preserve">
|
||||||
<value>(auto)</value>
|
<value>(auto)</value>
|
||||||
@@ -139,94 +142,118 @@
|
|||||||
<data name="Input_Custom" xml:space="preserve">
|
<data name="Input_Custom" xml:space="preserve">
|
||||||
<value>Custom</value>
|
<value>Custom</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_IgnoreOrientation" xml:space="preserve">
|
<data name="Input_IgnoreOrientation.Text" xml:space="preserve">
|
||||||
<value>Ignore the _orientation of pictures</value>
|
<value>Ignore the orientation of pictures</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_GifWarning" xml:space="preserve">
|
<data name="Input_GifWarning.Message" xml:space="preserve">
|
||||||
<value>Gif files with animations may not be correctly resized.</value>
|
<value>Gif files with animations may not be correctly resized.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_Replace" xml:space="preserve">
|
<data name="Input_Replace.Text" xml:space="preserve">
|
||||||
<value>Ov_erwrite files</value>
|
<value>Overwrite files</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_Resize" xml:space="preserve">
|
<data name="Input_Resize.Text" xml:space="preserve">
|
||||||
<value>Resize</value>
|
<value>Resize</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Input_ShrinkOnly" xml:space="preserve">
|
<data name="Input_ShrinkOnly.Text" xml:space="preserve">
|
||||||
<value>_Make pictures smaller but not larger</value>
|
<value>Make pictures smaller but not larger</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Large" xml:space="preserve">
|
<data name="Input_RemoveMetadata.Text" xml:space="preserve">
|
||||||
<value>Large</value>
|
<value>Remove metadata that doesn't affect rendering</value>
|
||||||
|
</data>
|
||||||
|
<data name="Image_Sizes" xml:space="preserve">
|
||||||
|
<value>Image sizes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Resize_Tooltip" xml:space="preserve">
|
||||||
|
<value>Resize pictures</value>
|
||||||
|
</data>
|
||||||
|
<data name="Resize_Type" xml:space="preserve">
|
||||||
|
<value>Resize type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Open_settings.Text" xml:space="preserve">
|
||||||
|
<value>Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="Open_settings_button.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
|
<value>Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="Input_AiSuperResolution" xml:space="preserve">
|
||||||
|
<value>Super resolution</value>
|
||||||
|
</data>
|
||||||
|
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
|
||||||
|
<value>Upscale images using on-device AI</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.Text" xml:space="preserve">
|
||||||
|
<value>Current:</value>
|
||||||
|
</data>
|
||||||
|
<data name="Input_AiNewLabel.Text" 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.Text" 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="Progress_MainInstruction.Text" xml:space="preserve">
|
||||||
|
<value>Resizing your pictures...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Progress_Stop.Text" xml:space="preserve">
|
||||||
|
<value>Stop</value>
|
||||||
|
</data>
|
||||||
|
<data name="Progress_TimeRemaining" xml:space="preserve">
|
||||||
|
<value>About {0} remaining.</value>
|
||||||
|
<comment>"About" = Approximately</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Results_MainInstruction.Text" xml:space="preserve">
|
||||||
|
<value>Can't resize the following pictures</value>
|
||||||
|
</data>
|
||||||
|
<data name="Results_Close.Text" xml:space="preserve">
|
||||||
|
<value>Close</value>
|
||||||
|
</data>
|
||||||
|
<data name="Small" xml:space="preserve">
|
||||||
|
<value>Small</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Medium" xml:space="preserve">
|
<data name="Medium" xml:space="preserve">
|
||||||
<value>Medium</value>
|
<value>Medium</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OK" xml:space="preserve">
|
<data name="Large" xml:space="preserve">
|
||||||
<value>OK</value>
|
<value>Large</value>
|
||||||
</data>
|
|
||||||
<data name="OK_Tooltip" xml:space="preserve">
|
|
||||||
<value>Apply settings</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="Phone" xml:space="preserve">
|
<data name="Phone" xml:space="preserve">
|
||||||
<value>Phone</value>
|
<value>Phone</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PictureFilter" xml:space="preserve">
|
|
||||||
<value>All Picture Files</value>
|
|
||||||
</data>
|
|
||||||
<data name="PngInterlaceOption_Default" xml:space="preserve">
|
|
||||||
<value>(Default)</value>
|
|
||||||
</data>
|
|
||||||
<data name="PngInterlaceOption_Off" xml:space="preserve">
|
|
||||||
<value>Off</value>
|
|
||||||
</data>
|
|
||||||
<data name="PngInterlaceOption_On" xml:space="preserve">
|
|
||||||
<value>On</value>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_MainInstruction" xml:space="preserve">
|
|
||||||
<value>Resizing your pictures...</value>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_Stop" xml:space="preserve">
|
|
||||||
<value>Stop</value>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_HourMinute" xml:space="preserve">
|
|
||||||
<value>About {0} hour, {1} minute remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_HourMinutes" xml:space="preserve">
|
|
||||||
<value>About {0} hour, {1} minutes remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_HoursMinute" xml:space="preserve">
|
|
||||||
<value>About {0} hours, {1} minute remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_HoursMinutes" xml:space="preserve">
|
|
||||||
<value>About {0} hours, {1} minutes remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_MinuteSecond" xml:space="preserve">
|
|
||||||
<value>About {1} minute, {2} second remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_MinuteSeconds" xml:space="preserve">
|
|
||||||
<value>About {1} minute, {2} seconds remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_MinutesSecond" xml:space="preserve">
|
|
||||||
<value>About {1} minutes, {2} second remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_MinutesSeconds" xml:space="preserve">
|
|
||||||
<value>About {1} minutes, {2} seconds remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_Second" xml:space="preserve">
|
|
||||||
<value>About {2} second remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="Progress_TimeRemaining_Seconds" xml:space="preserve">
|
|
||||||
<value>About {2} seconds remaining.</value>
|
|
||||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
|
||||||
</data>
|
|
||||||
<data name="ResizeFit_Fill" xml:space="preserve">
|
<data name="ResizeFit_Fill" xml:space="preserve">
|
||||||
<value>Fill</value>
|
<value>Fill</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -257,98 +284,15 @@
|
|||||||
<data name="ResizeUnit_Pixel" xml:space="preserve">
|
<data name="ResizeUnit_Pixel" xml:space="preserve">
|
||||||
<value>Pixels</value>
|
<value>Pixels</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Resize_Tooltip" xml:space="preserve">
|
<data name="PngInterlaceOption_Default" xml:space="preserve">
|
||||||
<value>Resize pictures</value>
|
<value>(Default)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Resize_Type" xml:space="preserve">
|
<data name="PngInterlaceOption_Off" xml:space="preserve">
|
||||||
<value>Resize type</value>
|
<value>Off</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Results_Close" xml:space="preserve">
|
<data name="PngInterlaceOption_On" xml:space="preserve">
|
||||||
<value>Close</value>
|
<value>On</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Results_MainInstruction" xml:space="preserve">
|
|
||||||
<value>Can't resize the following pictures</value>
|
|
||||||
</data>
|
|
||||||
<data name="Small" xml:space="preserve">
|
|
||||||
<value>Small</value>
|
|
||||||
</data>
|
|
||||||
<data name="Unit" xml:space="preserve">
|
|
||||||
<value>Unit</value>
|
|
||||||
</data>
|
|
||||||
<data name="ValueMustBeBetween" xml:space="preserve">
|
|
||||||
<value>Value must be between '{0}' and '{1}'.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Version" xml:space="preserve">
|
|
||||||
<value>Version</value>
|
|
||||||
</data>
|
|
||||||
<data name="Width" xml:space="preserve">
|
|
||||||
<value>Width</value>
|
|
||||||
</data>
|
|
||||||
<data name="Open_settings" xml:space="preserve">
|
|
||||||
<value>Settings</value>
|
|
||||||
</data>
|
|
||||||
<data name="Input_RemoveMetadata" xml:space="preserve">
|
|
||||||
<value>Remove meta_data that doesn't affect rendering</value>
|
|
||||||
</data>
|
|
||||||
<data name="Image_Sizes" xml:space="preserve">
|
|
||||||
<value>Image sizes</value>
|
|
||||||
</data>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- CLI Processing messages -->
|
|
||||||
<data name="CLI_ProcessingFiles" xml:space="preserve">
|
<data name="CLI_ProcessingFiles" xml:space="preserve">
|
||||||
<value>Processing {0} file(s)...</value>
|
<value>Processing {0} file(s)...</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -367,8 +311,6 @@
|
|||||||
<data name="CLI_NoInputFiles" xml:space="preserve">
|
<data name="CLI_NoInputFiles" xml:space="preserve">
|
||||||
<value>No input files or pipe specified. Showing usage.</value>
|
<value>No input files or pipe specified. Showing usage.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<!-- CLI Config display -->
|
|
||||||
<data name="CLI_ConfigTitle" xml:space="preserve">
|
<data name="CLI_ConfigTitle" xml:space="preserve">
|
||||||
<value>ImageResizer - Current Configuration</value>
|
<value>ImageResizer - Current Configuration</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -423,8 +365,6 @@
|
|||||||
<data name="CLI_ConfigCustomSelected" xml:space="preserve">
|
<data name="CLI_ConfigCustomSelected" xml:space="preserve">
|
||||||
<value> [Custom]* {0}x{1} {2} ({3})</value>
|
<value> [Custom]* {0}x{1} {2} ({3})</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<!-- CLI Usage help -->
|
|
||||||
<data name="CLI_UsageTitle" xml:space="preserve">
|
<data name="CLI_UsageTitle" xml:space="preserve">
|
||||||
<value>ImageResizer - PowerToys Image Resizer CLI</value>
|
<value>ImageResizer - PowerToys Image Resizer CLI</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -449,8 +389,6 @@
|
|||||||
<data name="CLI_UsageExamplePreset" xml:space="preserve">
|
<data name="CLI_UsageExamplePreset" xml:space="preserve">
|
||||||
<value> PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" photo.png</value>
|
<value> PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" photo.png</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<!-- CLI Option Descriptions -->
|
|
||||||
<data name="CLI_Option_Destination" xml:space="preserve">
|
<data name="CLI_Option_Destination" xml:space="preserve">
|
||||||
<value>Set destination directory</value>
|
<value>Set destination directory</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -499,4 +437,10 @@
|
|||||||
<data name="CLI_Option_Width" xml:space="preserve">
|
<data name="CLI_Option_Width" xml:space="preserve">
|
||||||
<value>Set width</value>
|
<value>Set width</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ValueMustBeBetween" xml:space="preserve">
|
||||||
|
<value>Value must be between '{0}' and '{1}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TitleBarTitle.Title" xml:space="preserve">
|
||||||
|
<value>Image Resizer</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
91
src/modules/imageresizer/ui/Utilities/CodecHelper.cs
Normal file
91
src/modules/imageresizer/ui/Utilities/CodecHelper.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Windows.Graphics.Imaging;
|
||||||
|
|
||||||
|
namespace ImageResizer.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps between legacy container format GUIDs (used in settings JSON) and WinRT encoder/decoder IDs,
|
||||||
|
/// and provides file extension lookups.
|
||||||
|
/// </summary>
|
||||||
|
internal static class CodecHelper
|
||||||
|
{
|
||||||
|
// Legacy container format GUID (stored in settings JSON) -> WinRT Encoder ID
|
||||||
|
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
|
||||||
|
{
|
||||||
|
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
|
||||||
|
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
|
||||||
|
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
|
||||||
|
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
|
||||||
|
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// WinRT Decoder ID -> WinRT Encoder ID
|
||||||
|
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
|
||||||
|
{
|
||||||
|
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
|
||||||
|
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
|
||||||
|
[BitmapDecoder.BmpDecoderId] = BitmapEncoder.BmpEncoderId,
|
||||||
|
[BitmapDecoder.TiffDecoderId] = BitmapEncoder.TiffEncoderId,
|
||||||
|
[BitmapDecoder.GifDecoderId] = BitmapEncoder.GifEncoderId,
|
||||||
|
[BitmapDecoder.JpegXRDecoderId] = BitmapEncoder.JpegXREncoderId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encoder ID -> supported file extensions
|
||||||
|
private static readonly Dictionary<Guid, string[]> EncoderExtensions = new()
|
||||||
|
{
|
||||||
|
[BitmapEncoder.JpegEncoderId] = [".jpg", ".jpeg", ".jpe", ".jfif"],
|
||||||
|
[BitmapEncoder.PngEncoderId] = [".png"],
|
||||||
|
[BitmapEncoder.BmpEncoderId] = [".bmp", ".dib", ".rle"],
|
||||||
|
[BitmapEncoder.TiffEncoderId] = [".tiff", ".tif"],
|
||||||
|
[BitmapEncoder.GifEncoderId] = [".gif"],
|
||||||
|
[BitmapEncoder.JpegXREncoderId] = [".jxr", ".wdp"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the WinRT encoder ID that corresponds to the given legacy container format GUID.
|
||||||
|
/// Falls back to PNG if the GUID is not recognized.
|
||||||
|
/// </summary>
|
||||||
|
public static Guid GetEncoderIdFromLegacyGuid(Guid containerFormatGuid)
|
||||||
|
=> LegacyGuidToEncoderId.TryGetValue(containerFormatGuid, out var id)
|
||||||
|
? id
|
||||||
|
: BitmapEncoder.PngEncoderId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the WinRT encoder ID that matches the given decoder's codec.
|
||||||
|
/// Returns null if no matching encoder exists (e.g., ICO decoder has no encoder).
|
||||||
|
/// </summary>
|
||||||
|
public static Guid? GetEncoderIdForDecoder(BitmapDecoder decoder)
|
||||||
|
{
|
||||||
|
var codecId = decoder.DecoderInformation?.CodecId ?? Guid.Empty;
|
||||||
|
return DecoderIdToEncoderId.TryGetValue(codecId, out var encoderId)
|
||||||
|
? encoderId
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the supported file extensions for the given encoder ID.
|
||||||
|
/// </summary>
|
||||||
|
public static string[] GetSupportedExtensions(Guid encoderId)
|
||||||
|
=> EncoderExtensions.TryGetValue(encoderId, out var extensions)
|
||||||
|
? extensions
|
||||||
|
: [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the default (first) file extension for the given encoder ID.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDefaultExtension(Guid encoderId)
|
||||||
|
=> GetSupportedExtensions(encoderId).FirstOrDefault() ?? ".png";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the given encoder ID is a known, supported encoder.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CanEncode(Guid encoderId)
|
||||||
|
=> EncoderExtensions.ContainsKey(encoderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace ImageResizer.Utilities
|
|
||||||
{
|
|
||||||
internal static class MathHelpers
|
|
||||||
{
|
|
||||||
public static int Clamp(int value, int min, int max)
|
|
||||||
=> Math.Min(Math.Max(value, min), max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// 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;
|
|
||||||
|
|
||||||
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "File created under PowerToys.")]
|
|
||||||
|
|
||||||
namespace ImageResizer.Utilities
|
|
||||||
{
|
|
||||||
// Win32 functions required for temporary workaround for issue #1273
|
|
||||||
internal class NativeMethods
|
|
||||||
{
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
internal static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
|
||||||
internal static extern bool SetProcessDPIAware();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
|
|
||||||
using ImageResizer.Helpers;
|
|
||||||
using ImageResizer.Models;
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
|
||||||
{
|
|
||||||
public class AdvancedViewModel : Observable
|
|
||||||
{
|
|
||||||
private static Dictionary<Guid, string> InitEncoderMap()
|
|
||||||
{
|
|
||||||
var bmpCodec = new BmpBitmapEncoder().CodecInfo;
|
|
||||||
var gifCodec = new GifBitmapEncoder().CodecInfo;
|
|
||||||
var jpegCodec = new JpegBitmapEncoder().CodecInfo;
|
|
||||||
var pngCodec = new PngBitmapEncoder().CodecInfo;
|
|
||||||
var tiffCodec = new TiffBitmapEncoder().CodecInfo;
|
|
||||||
var wmpCodec = new WmpBitmapEncoder().CodecInfo;
|
|
||||||
|
|
||||||
return new Dictionary<Guid, string>
|
|
||||||
{
|
|
||||||
[bmpCodec.ContainerFormat] = bmpCodec.FriendlyName,
|
|
||||||
[gifCodec.ContainerFormat] = gifCodec.FriendlyName,
|
|
||||||
[jpegCodec.ContainerFormat] = jpegCodec.FriendlyName,
|
|
||||||
[pngCodec.ContainerFormat] = pngCodec.FriendlyName,
|
|
||||||
[tiffCodec.ContainerFormat] = tiffCodec.FriendlyName,
|
|
||||||
[wmpCodec.ContainerFormat] = wmpCodec.FriendlyName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public AdvancedViewModel(Settings settings)
|
|
||||||
{
|
|
||||||
RemoveSizeCommand = new RelayCommand<ResizeSize>(RemoveSize);
|
|
||||||
AddSizeCommand = new RelayCommand(AddSize);
|
|
||||||
Settings = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IDictionary<Guid, string> EncoderMap { get; } = InitEncoderMap();
|
|
||||||
|
|
||||||
public Settings Settings { get; }
|
|
||||||
|
|
||||||
public static string Version
|
|
||||||
=> typeof(AdvancedViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
|
||||||
?.InformationalVersion;
|
|
||||||
|
|
||||||
public static IEnumerable<Guid> Encoders => EncoderMap.Keys;
|
|
||||||
|
|
||||||
public ICommand RemoveSizeCommand { get; }
|
|
||||||
|
|
||||||
public ICommand AddSizeCommand { get; }
|
|
||||||
|
|
||||||
public void RemoveSize(ResizeSize size)
|
|
||||||
=> Settings.Sizes.Remove(size);
|
|
||||||
|
|
||||||
public void AddSize()
|
|
||||||
=> Settings.Sizes.Add(new ResizeSize());
|
|
||||||
|
|
||||||
public void Close(bool accepted)
|
|
||||||
{
|
|
||||||
if (accepted)
|
|
||||||
{
|
|
||||||
Settings.Save();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedSizeIndex = Settings.SelectedSizeIndex;
|
|
||||||
var shrinkOnly = Settings.ShrinkOnly;
|
|
||||||
var replace = Settings.Replace;
|
|
||||||
var ignoreOrientation = Settings.IgnoreOrientation;
|
|
||||||
|
|
||||||
Settings.Reload();
|
|
||||||
Settings.SelectedSizeIndex = selectedSizeIndex;
|
|
||||||
Settings.ShrinkOnly = shrinkOnly;
|
|
||||||
Settings.Replace = replace;
|
|
||||||
Settings.IgnoreOrientation = ignoreOrientation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
|
||||||
{
|
|
||||||
public interface ITabViewModel
|
|
||||||
{
|
|
||||||
string Header { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -10,11 +10,12 @@ using System.ComponentModel;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using Windows.Graphics.Imaging;
|
||||||
using System.Windows.Media.Imaging;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Common.UI;
|
using Common.UI;
|
||||||
|
using ManagedCommon;
|
||||||
using ImageResizer.Helpers;
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
@@ -23,25 +24,12 @@ using ImageResizer.Views;
|
|||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
namespace ImageResizer.ViewModels
|
||||||
{
|
{
|
||||||
public class InputViewModel : Observable
|
public partial class InputViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
public const int DefaultAiScale = 2;
|
public const int DefaultAiScale = 2;
|
||||||
private const int MinAiScale = 1;
|
private const int MinAiScale = 1;
|
||||||
private const int MaxAiScale = 8;
|
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
|
public enum Dimension
|
||||||
{
|
{
|
||||||
Width,
|
Width,
|
||||||
@@ -55,6 +43,27 @@ namespace ImageResizer.ViewModels
|
|||||||
public Dimension Dimension { get; set; }
|
public Dimension Dimension { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly ResizeBatch _batch;
|
||||||
|
private readonly MainViewModel _mainViewModel;
|
||||||
|
private readonly IMainView _mainView;
|
||||||
|
private readonly bool _hasGifFiles;
|
||||||
|
private readonly bool _hasMultipleFiles;
|
||||||
|
private bool _originalDimensionsLoaded;
|
||||||
|
private int? _originalWidth;
|
||||||
|
private int? _originalHeight;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _currentResolutionDescription;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _newResolutionDescription;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isDownloadingModel;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _modelStatusMessage;
|
||||||
|
|
||||||
public InputViewModel(
|
public InputViewModel(
|
||||||
Settings settings,
|
Settings settings,
|
||||||
MainViewModel mainViewModel,
|
MainViewModel mainViewModel,
|
||||||
@@ -64,6 +73,7 @@ namespace ImageResizer.ViewModels
|
|||||||
_batch = batch;
|
_batch = batch;
|
||||||
_mainViewModel = mainViewModel;
|
_mainViewModel = mainViewModel;
|
||||||
_mainView = mainView;
|
_mainView = mainView;
|
||||||
|
_hasGifFiles = _batch?.Files.Any(filename => filename.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) == true;
|
||||||
_hasMultipleFiles = _batch?.Files.Count > 1;
|
_hasMultipleFiles = _batch?.Files.Count > 1;
|
||||||
|
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
@@ -80,13 +90,6 @@ namespace ImageResizer.ViewModels
|
|||||||
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
InitializeAiState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,94 +114,44 @@ namespace ImageResizer.ViewModels
|
|||||||
|
|
||||||
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
||||||
|
|
||||||
public string CurrentResolutionDescription
|
public bool IsCustomSizeSelected => Settings?.SelectedSize is CustomSize;
|
||||||
{
|
|
||||||
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;
|
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 =>
|
public bool ShowModelDownloadPrompt =>
|
||||||
Settings?.SelectedSize is AiSize &&
|
Settings?.SelectedSize is AiSize &&
|
||||||
(App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
|
(App.AiAvailabilityState == AiAvailabilityState.ModelNotReady || IsDownloadingModel);
|
||||||
|
|
||||||
// Show AI controls when: AI size is selected and AI is ready
|
|
||||||
public bool ShowAiControls =>
|
public bool ShowAiControls =>
|
||||||
Settings?.SelectedSize is AiSize &&
|
Settings?.SelectedSize is AiSize &&
|
||||||
App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
App.AiAvailabilityState == AiAvailabilityState.Ready;
|
||||||
|
|
||||||
|
public bool ShowAiConfigurationSection => ShowModelDownloadPrompt || ShowAiControls;
|
||||||
|
|
||||||
/// <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
|
public bool CanResize
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
// If AI size is selected, only allow resize when AI is fully ready
|
|
||||||
if (Settings?.SelectedSize is AiSize)
|
if (Settings?.SelectedSize is AiSize)
|
||||||
{
|
{
|
||||||
return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
return App.AiAvailabilityState == AiAvailabilityState.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-AI resize can always proceed
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand ResizeCommand { get; }
|
public bool HasGifFiles => _hasGifFiles;
|
||||||
|
|
||||||
public ICommand CancelCommand { get; }
|
|
||||||
|
|
||||||
public ICommand OpenSettingsCommand { get; }
|
|
||||||
|
|
||||||
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)) == true;
|
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanResize))]
|
||||||
public void Resize()
|
public void Resize()
|
||||||
{
|
{
|
||||||
Settings.Save();
|
Settings.Save();
|
||||||
_mainViewModel.CurrentPage = new ProgressViewModel(_batch, _mainViewModel, _mainView);
|
_mainViewModel.CurrentPage = new ProgressViewModel(_batch, _mainViewModel, _mainView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void OpenSettings()
|
[RelayCommand]
|
||||||
{
|
private void EnterKeyPressed(KeyPressParams parameters)
|
||||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleEnterKeyPress(KeyPressParams parameters)
|
|
||||||
{
|
{
|
||||||
switch (parameters.Dimension)
|
switch (parameters.Dimension)
|
||||||
{
|
{
|
||||||
@@ -211,25 +164,68 @@ namespace ImageResizer.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void Cancel()
|
public void Cancel()
|
||||||
=> _mainView.Close();
|
=> _mainView.Close();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public static void OpenSettings()
|
||||||
|
{
|
||||||
|
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task DownloadModelAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsDownloadingModel = true;
|
||||||
|
ModelStatusMessage = ResourceLoaderInstance.GetString("Input_AiModelDownloading");
|
||||||
|
NotifyAiStateChanged();
|
||||||
|
|
||||||
|
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync();
|
||||||
|
|
||||||
|
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||||
|
{
|
||||||
|
App.AiAvailabilityState = AiAvailabilityState.Ready;
|
||||||
|
UpdateStatusMessage();
|
||||||
|
|
||||||
|
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||||
|
if (aiService != null)
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModelStatusMessage = ResourceLoaderInstance.GetString("Input_AiModelDownloadFailed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"AI model download failed: {ex.Message}");
|
||||||
|
ModelStatusMessage = ResourceLoaderInstance.GetString("Input_AiModelDownloadFailed");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsDownloadingModel = false;
|
||||||
|
NotifyAiStateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.PropertyName)
|
switch (e.PropertyName)
|
||||||
{
|
{
|
||||||
case nameof(Settings.SelectedSizeIndex):
|
case nameof(Settings.SelectedSizeIndex):
|
||||||
case nameof(Settings.SelectedSize):
|
OnPropertyChanged(nameof(IsCustomSizeSelected));
|
||||||
// Notify UI state properties that depend on SelectedSize
|
|
||||||
NotifyAiStateChanged();
|
NotifyAiStateChanged();
|
||||||
UpdateAiDetails();
|
UpdateAiDetails();
|
||||||
|
ResizeCommand.NotifyCanExecuteChanged();
|
||||||
// Trigger CanExecuteChanged for ResizeCommand
|
|
||||||
if (ResizeCommand is RelayCommand cmd)
|
|
||||||
{
|
|
||||||
cmd.OnCanExecuteChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,51 +234,54 @@ namespace ImageResizer.ViewModels
|
|||||||
{
|
{
|
||||||
if (Settings?.AiSize != null)
|
if (Settings?.AiSize != null)
|
||||||
{
|
{
|
||||||
Settings.AiSize.Scale = Math.Clamp(
|
Settings.AiSize.Scale = Math.Clamp(Settings.AiSize.Scale, MinAiScale, MaxAiScale);
|
||||||
Settings.AiSize.Scale,
|
|
||||||
MinAiScale,
|
|
||||||
MaxAiScale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAiDetails()
|
private async void UpdateAiDetails()
|
||||||
{
|
{
|
||||||
// Clear AI details if AI size not selected
|
try
|
||||||
if (Settings == null || Settings.SelectedSize is not AiSize)
|
|
||||||
{
|
{
|
||||||
CurrentResolutionDescription = string.Empty;
|
if (Settings == null || Settings.SelectedSize is not AiSize)
|
||||||
NewResolutionDescription = string.Empty;
|
{
|
||||||
return;
|
CurrentResolutionDescription = string.Empty;
|
||||||
|
NewResolutionDescription = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureAiScaleWithinRange();
|
||||||
|
|
||||||
|
if (_hasMultipleFiles)
|
||||||
|
{
|
||||||
|
CurrentResolutionDescription = string.Empty;
|
||||||
|
NewResolutionDescription = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureOriginalDimensionsLoadedAsync();
|
||||||
|
|
||||||
|
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
|
||||||
|
CurrentResolutionDescription = hasConcreteSize
|
||||||
|
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||||
|
: ResourceLoaderInstance.GetString("Input_AiUnknownSize");
|
||||||
|
|
||||||
|
var scale = Settings.AiSize.Scale;
|
||||||
|
NewResolutionDescription = hasConcreteSize
|
||||||
|
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||||
|
: ResourceLoaderInstance.GetString("Input_AiUnknownSize");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
EnsureAiScaleWithinRange();
|
|
||||||
|
|
||||||
if (_hasMultipleFiles)
|
|
||||||
{
|
{
|
||||||
CurrentResolutionDescription = string.Empty;
|
Logger.LogError($"UpdateAiDetails failed: {ex.Message}");
|
||||||
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)
|
private static string FormatDimensions(long width, long height)
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
|
return string.Format(CultureInfo.CurrentCulture, "{0} x {1}", width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureOriginalDimensionsLoaded()
|
private async Task EnsureOriginalDimensionsLoadedAsync()
|
||||||
{
|
{
|
||||||
if (_originalDimensionsLoaded)
|
if (_originalDimensionsLoaded)
|
||||||
{
|
{
|
||||||
@@ -298,18 +297,15 @@ namespace ImageResizer.ViewModels
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.OpenRead(file);
|
using var fileStream = File.OpenRead(file);
|
||||||
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
|
using var stream = fileStream.AsRandomAccessStream();
|
||||||
var frame = decoder.Frames.FirstOrDefault();
|
var decoder = await BitmapDecoder.CreateAsync(stream);
|
||||||
if (frame != null)
|
_originalWidth = (int)decoder.PixelWidth;
|
||||||
{
|
_originalHeight = (int)decoder.PixelHeight;
|
||||||
_originalWidth = frame.PixelWidth;
|
|
||||||
_originalHeight = frame.PixelHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Failed to load image dimensions - clear values
|
Logger.LogWarning($"Failed to read image dimensions for {file}: {ex.Message}");
|
||||||
_originalWidth = null;
|
_originalWidth = null;
|
||||||
_originalHeight = null;
|
_originalHeight = null;
|
||||||
}
|
}
|
||||||
@@ -319,128 +315,44 @@ namespace ImageResizer.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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()
|
private void InitializeAiState()
|
||||||
{
|
{
|
||||||
// Subscribe to initialization completion event to refresh UI
|
|
||||||
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
||||||
|
|
||||||
// Set initial status message based on current state
|
|
||||||
UpdateStatusMessage();
|
UpdateStatusMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void OnAiInitializationCompleted(object sender, AiAvailabilityState finalState)
|
||||||
/// Handles AI initialization completion event from App.
|
|
||||||
/// Refreshes UI when background initialization finishes.
|
|
||||||
/// </summary>
|
|
||||||
private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
|
|
||||||
{
|
{
|
||||||
UpdateStatusMessage();
|
UpdateStatusMessage();
|
||||||
NotifyAiStateChanged();
|
NotifyAiStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates status message based on current App availability state.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateStatusMessage()
|
private void UpdateStatusMessage()
|
||||||
{
|
{
|
||||||
ModelStatusMessage = App.AiAvailabilityState switch
|
ModelStatusMessage = App.AiAvailabilityState switch
|
||||||
{
|
{
|
||||||
Properties.AiAvailabilityState.Ready => string.Empty,
|
AiAvailabilityState.Ready => string.Empty,
|
||||||
Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
|
AiAvailabilityState.ModelNotReady => ResourceLoaderInstance.GetString("Input_AiModelNotAvailable"),
|
||||||
Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
|
AiAvailabilityState.NotSupported => ResourceLoaderInstance.GetString("Input_AiModelNotSupported"),
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notifies UI when AI state changes (model availability, download status).
|
|
||||||
/// </summary>
|
|
||||||
private void NotifyAiStateChanged()
|
private void NotifyAiStateChanged()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsModelDownloading));
|
OnPropertyChanged(nameof(IsDownloadingModel));
|
||||||
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
||||||
OnPropertyChanged(nameof(ShowAiControls));
|
OnPropertyChanged(nameof(ShowAiControls));
|
||||||
|
OnPropertyChanged(nameof(ShowAiConfigurationSection));
|
||||||
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
||||||
OnPropertyChanged(nameof(CanResize));
|
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()
|
private void NotifyAiScaleChanged()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
||||||
OnPropertyChanged(nameof(AiScaleDisplay));
|
OnPropertyChanged(nameof(AiScaleDisplay));
|
||||||
UpdateAiDetails();
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,41 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ImageResizer.Helpers;
|
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Properties;
|
using ImageResizer.Properties;
|
||||||
using ImageResizer.Views;
|
using ImageResizer.Views;
|
||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
namespace ImageResizer.ViewModels
|
||||||
{
|
{
|
||||||
public class MainViewModel : Observable
|
public partial class MainViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly Settings _settings;
|
private readonly Settings _settings;
|
||||||
private readonly ResizeBatch _batch;
|
private readonly ResizeBatch _batch;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
private object _currentPage;
|
private object _currentPage;
|
||||||
private double _progress;
|
|
||||||
|
|
||||||
public MainViewModel(ResizeBatch batch, Settings settings)
|
public MainViewModel(ResizeBatch batch, Settings settings)
|
||||||
{
|
{
|
||||||
_batch = batch;
|
_batch = batch;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
LoadCommand = new RelayCommand<IMainView>(Load);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand LoadCommand { get; }
|
[RelayCommand]
|
||||||
|
public async Task LoadAsync(IMainView view)
|
||||||
public object CurrentPage
|
|
||||||
{
|
|
||||||
get => _currentPage;
|
|
||||||
set => Set(ref _currentPage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Progress
|
|
||||||
{
|
|
||||||
get => _progress;
|
|
||||||
set => Set(ref _progress, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load(IMainView view)
|
|
||||||
{
|
{
|
||||||
if (_batch.Files.Count == 0)
|
if (_batch.Files.Count == 0)
|
||||||
{
|
{
|
||||||
_batch.Files.AddRange(view.OpenPictureFiles());
|
foreach (var file in await view.OpenPictureFilesAsync())
|
||||||
|
{
|
||||||
|
_batch.Files.Add(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentPage = new InputViewModel(_settings, this, view, _batch);
|
CurrentPage = new InputViewModel(_settings, this, view, _batch);
|
||||||
|
|||||||
@@ -1,33 +1,59 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ImageResizer.Helpers;
|
using ImageResizer.Helpers;
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Views;
|
using ImageResizer.Views;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
namespace ImageResizer.ViewModels
|
||||||
{
|
{
|
||||||
public class ProgressViewModel : Observable, IDisposable
|
public partial class ProgressViewModel : ObservableObject, IDisposable
|
||||||
{
|
{
|
||||||
private readonly MainViewModel _mainViewModel;
|
private readonly MainViewModel _mainViewModel;
|
||||||
private readonly ResizeBatch _batch;
|
private readonly ResizeBatch _batch;
|
||||||
private readonly IMainView _mainView;
|
private readonly IMainView _mainView;
|
||||||
private readonly Stopwatch _stopwatch = new Stopwatch();
|
private readonly Stopwatch _stopwatch = new();
|
||||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
|
||||||
|
private bool _disposedValue;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
private double _progress;
|
private double _progress;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TimeRemainingDisplay))]
|
||||||
private TimeSpan _timeRemaining;
|
private TimeSpan _timeRemaining;
|
||||||
private bool disposedValue;
|
|
||||||
|
private static CompositeFormat _progressTimeRemainingFormat;
|
||||||
|
|
||||||
|
private static CompositeFormat ProgressTimeRemainingFormat =>
|
||||||
|
_progressTimeRemainingFormat ??= CompositeFormat.Parse(ResourceLoaderInstance.GetString("Progress_TimeRemaining"));
|
||||||
|
|
||||||
|
public string TimeRemainingDisplay
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (TimeRemaining == TimeSpan.MaxValue || TimeRemaining.TotalSeconds < 1)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, ProgressTimeRemainingFormat, TimeRemaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ProgressViewModel(
|
public ProgressViewModel(
|
||||||
ResizeBatch batch,
|
ResizeBatch batch,
|
||||||
@@ -37,57 +63,49 @@ namespace ImageResizer.ViewModels
|
|||||||
_batch = batch;
|
_batch = batch;
|
||||||
_mainViewModel = mainViewModel;
|
_mainViewModel = mainViewModel;
|
||||||
_mainView = mainView;
|
_mainView = mainView;
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
StartCommand = new RelayCommand(Start);
|
|
||||||
StopCommand = new RelayCommand(Stop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double Progress
|
[RelayCommand]
|
||||||
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
get => _progress;
|
try
|
||||||
set => Set(ref _progress, value);
|
{
|
||||||
}
|
_stopwatch.Restart();
|
||||||
|
var errors = await _batch.ProcessAsync(
|
||||||
|
(completed, total) =>
|
||||||
|
{
|
||||||
|
var progress = completed / total;
|
||||||
|
var timeRemaining = _stopwatch.Elapsed.Multiply((total - completed) / completed);
|
||||||
|
|
||||||
public TimeSpan TimeRemaining
|
// Progress callback runs on thread-pool threads (from Parallel.ForEachAsync),
|
||||||
{
|
// so we must dispatch UI property updates to the UI thread.
|
||||||
get => _timeRemaining;
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
set => Set(ref _timeRemaining, value);
|
{
|
||||||
}
|
Progress = progress;
|
||||||
|
TimeRemaining = timeRemaining;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_cancellationTokenSource.Token);
|
||||||
|
|
||||||
public ICommand StartCommand { get; }
|
// After await we are back on the UI thread (SynchronizationContext),
|
||||||
|
// so we can update UI directly without DispatcherQueue.
|
||||||
public ICommand StopCommand { get; }
|
if (errors.Any())
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
_ = Task.Factory.StartNew(StartExecutingWork, _cancellationTokenSource.Token, TaskCreationOptions.None, TaskScheduler.Current);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartExecutingWork()
|
|
||||||
{
|
|
||||||
_stopwatch.Restart();
|
|
||||||
var errors = _batch.Process(
|
|
||||||
(completed, total) =>
|
|
||||||
{
|
{
|
||||||
var progress = completed / total;
|
_mainViewModel.CurrentPage = new ResultsViewModel(_mainView, errors);
|
||||||
Progress = progress;
|
}
|
||||||
_mainViewModel.Progress = progress;
|
else
|
||||||
|
{
|
||||||
TimeRemaining = _stopwatch.Elapsed.Multiply((total - completed) / completed);
|
_mainView.Close();
|
||||||
},
|
}
|
||||||
_cancellationTokenSource.Token);
|
|
||||||
|
|
||||||
if (errors.Any())
|
|
||||||
{
|
|
||||||
_mainViewModel.Progress = 0;
|
|
||||||
_mainViewModel.CurrentPage = new ResultsViewModel(_mainView, errors);
|
|
||||||
}
|
}
|
||||||
else
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_mainView.Close();
|
// User cancelled via Stop — window is already closing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
@@ -96,20 +114,19 @@ namespace ImageResizer.ViewModels
|
|||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!disposedValue)
|
if (!_disposedValue)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
disposedValue = true;
|
_disposedValue = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
#pragma warning disable IDE0073
|
#pragma warning disable IDE0073, SA1636
|
||||||
// Copyright (c) Brice Lambson
|
// Copyright (c) Brice Lambson
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
// The Brice Lambson licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||||
#pragma warning restore IDE0073
|
#pragma warning restore IDE0073, SA1636
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Windows.Input;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ImageResizer.Helpers;
|
|
||||||
using ImageResizer.Models;
|
using ImageResizer.Models;
|
||||||
using ImageResizer.Views;
|
using ImageResizer.Views;
|
||||||
|
|
||||||
namespace ImageResizer.ViewModels
|
namespace ImageResizer.ViewModels
|
||||||
{
|
{
|
||||||
public class ResultsViewModel : Observable
|
public partial class ResultsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IMainView _mainView;
|
private readonly IMainView _mainView;
|
||||||
|
|
||||||
@@ -21,13 +19,11 @@ namespace ImageResizer.ViewModels
|
|||||||
{
|
{
|
||||||
_mainView = mainView;
|
_mainView = mainView;
|
||||||
Errors = errors;
|
Errors = errors;
|
||||||
CloseCommand = new RelayCommand(Close);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ResizeError> Errors { get; }
|
public IEnumerable<ResizeError> Errors { get; }
|
||||||
|
|
||||||
public ICommand CloseCommand { get; }
|
[RelayCommand]
|
||||||
|
|
||||||
public void Close() => _mainView.Close();
|
public void Close() => _mainView.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
// 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.Globalization;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public class AccessTextToTextConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
return ((string)value).Replace("_", string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts between double and string for text-based controls bound to Width or Height fields.
|
|
||||||
/// Optionally returns localized "Auto" text when the underlying value is 0, letting the UI show,
|
|
||||||
/// for example "(auto) x 1024 pixels".
|
|
||||||
/// </summary>
|
|
||||||
[ValueConversion(typeof(double), typeof(string))]
|
|
||||||
internal class AutoDoubleConverter : IValueConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are
|
|
||||||
/// converted to empty strings.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to convert from <see cref="double"/> to
|
|
||||||
/// <see cref="string"/>.</param>
|
|
||||||
/// <param name="targetType">The conversion target type. <see cref="string"/> here.</param>
|
|
||||||
/// <param name="parameter">Set to "Auto" to return the localized "Auto" string if the
|
|
||||||
/// value is 0.</param>
|
|
||||||
/// <param name="culture">The <see cref="CultureInfo"/> to use for the number formatting.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>The string representation of the passed-in value.</returns>
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|
||||||
value switch
|
|
||||||
{
|
|
||||||
double d => d switch
|
|
||||||
{
|
|
||||||
double.NaN => "0",
|
|
||||||
0 => (string)parameter == "Auto" ? Resources.Input_Auto : "0",
|
|
||||||
_ => d.ToString(culture),
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => "0",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the string representation back to a double, returning 0 if the string is empty,
|
|
||||||
/// null or not a valid number in the specified culture.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The string value to convert.</param>
|
|
||||||
/// <param name="targetType">The conversion target type. <see cref="double"/> here.</param>
|
|
||||||
/// <param name="parameter">Converter parameter. Unused.</param>
|
|
||||||
/// <param name="culture">The <see cref="CultureInfo"/> to use for the text parsing.</param>
|
|
||||||
/// <returns>The corresponding double value.</returns>
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|
||||||
value switch
|
|
||||||
{
|
|
||||||
null or "" => 0,
|
|
||||||
string text when double.TryParse(text, NumberStyles.Any, culture, out double result) => result,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
|
||||||
internal class BoolValueConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(Enum), typeof(int))]
|
|
||||||
internal class EnumToIntConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> (int)value;
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> targetType.GetEnumValues().GetValue((int)value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public interface IMainView
|
|
||||||
{
|
|
||||||
void Close();
|
|
||||||
|
|
||||||
IEnumerable<string> OpenPictureFiles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
x:Class="ImageResizer.Views.InputPage"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:m="clr-namespace:ImageResizer.Models"
|
|
||||||
xmlns:p="clr-namespace:ImageResizer.Properties"
|
|
||||||
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" />
|
|
||||||
<!-- ComboBox -->
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<!-- other controls -->
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.Resources>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
<Grid Grid.Row="1">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Border
|
|
||||||
Grid.RowSpan="5"
|
|
||||||
Background="{DynamicResource LayerFillColorDefaultBrush}"
|
|
||||||
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>
|
|
||||||
<ColumnDefinition Width="24" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="24" />
|
|
||||||
<ColumnDefinition Width="24" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="8" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<ui:SymbolIcon
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Width}"
|
|
||||||
FontSize="20"
|
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
|
||||||
Symbol="AutoFitWidth20"
|
|
||||||
ToolTipService.ToolTip="{x:Static p:Resources.Width}" />
|
|
||||||
<ui:NumberBox
|
|
||||||
Name="WidthNumberBox"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Width}"
|
|
||||||
KeyDown="Button_KeyDown"
|
|
||||||
Minimum="0"
|
|
||||||
SpinButtonPlacementMode="Inline">
|
|
||||||
<ui:NumberBox.NumberFormatter>
|
|
||||||
<v:ZeroToEmptyStringNumberFormatter />
|
|
||||||
</ui:NumberBox.NumberFormatter>
|
|
||||||
<ui:NumberBox.Value>
|
|
||||||
<Binding
|
|
||||||
Converter="{StaticResource NumberBoxValueConverter}"
|
|
||||||
ElementName="SizeComboBox"
|
|
||||||
Mode="TwoWay"
|
|
||||||
Path="SelectedValue.Width"
|
|
||||||
UpdateSourceTrigger="PropertyChanged" />
|
|
||||||
</ui:NumberBox.Value>
|
|
||||||
</ui:NumberBox>
|
|
||||||
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Grid.Column="3"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Height}"
|
|
||||||
FontSize="20"
|
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
|
||||||
Symbol="AutoFitHeight20"
|
|
||||||
ToolTipService.ToolTip="{x:Static p:Resources.Height}"
|
|
||||||
Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue.ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
|
||||||
<ui:NumberBox
|
|
||||||
Name="HeightNumberBox"
|
|
||||||
Grid.Column="4"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Height}"
|
|
||||||
DataContext="{Binding ElementName=SizeComboBox, Path=SelectedItem}"
|
|
||||||
KeyDown="Button_KeyDown"
|
|
||||||
Minimum="0"
|
|
||||||
SpinButtonPlacementMode="Inline"
|
|
||||||
Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue.ShowHeight, Converter={StaticResource BoolValueConverter}}">
|
|
||||||
<ui:NumberBox.NumberFormatter>
|
|
||||||
<v:ZeroToEmptyStringNumberFormatter />
|
|
||||||
</ui:NumberBox.NumberFormatter>
|
|
||||||
<ui:NumberBox.Value>
|
|
||||||
<Binding
|
|
||||||
Converter="{StaticResource NumberBoxValueConverter}"
|
|
||||||
ElementName="SizeComboBox"
|
|
||||||
Mode="TwoWay"
|
|
||||||
Path="SelectedValue.Height"
|
|
||||||
UpdateSourceTrigger="PropertyChanged" />
|
|
||||||
</ui:NumberBox.Value>
|
|
||||||
</ui:NumberBox>
|
|
||||||
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Grid.Row="2"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="20"
|
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
|
||||||
Symbol="Crop20"
|
|
||||||
ToolTipService.ToolTip="{x:Static p:Resources.Resize_Type}" />
|
|
||||||
<ComboBox
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Resize_Type}"
|
|
||||||
ItemsSource="{Binding ResizeFitValues, Mode=OneWay}"
|
|
||||||
SelectedIndex="{Binding ElementName=SizeComboBox, Path=SelectedValue.Fit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate DataType="{x:Type m:ResizeFit}">
|
|
||||||
<TextBlock Padding="2,0" Text="{Binding {}, Converter={StaticResource EnumValueConverter}}" />
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<ui:SymbolIcon
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="3"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="20"
|
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
|
||||||
Symbol="Ruler20"
|
|
||||||
ToolTipService.ToolTip="{x:Static p:Resources.Unit}" />
|
|
||||||
<ComboBox
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="4"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Unit}"
|
|
||||||
ItemsSource="{Binding ResizeUnitValues, Mode=OneWay}"
|
|
||||||
SelectedIndex="{Binding ElementName=SizeComboBox, Path=SelectedValue.Unit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate DataType="{x:Type m:ResizeUnit}">
|
|
||||||
<TextBlock Padding="2,0" Text="{Binding {}, Converter={StaticResource EnumValueConverter}}" />
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- CheckBoxes -->
|
|
||||||
<StackPanel
|
|
||||||
Grid.Row="1"
|
|
||||||
Margin="16"
|
|
||||||
Orientation="Vertical">
|
|
||||||
<CheckBox AutomationProperties.Name="{Binding RelativeSource={RelativeSource Self}, Path=Content.(AccessText.Text), Converter={StaticResource AccessTextToTextConverter}}" IsChecked="{Binding Settings.ShrinkOnly}">
|
|
||||||
<CheckBox.Content>
|
|
||||||
<AccessText Text="{x:Static p:Resources.Input_ShrinkOnly}" TextWrapping="Wrap" />
|
|
||||||
</CheckBox.Content>
|
|
||||||
</CheckBox>
|
|
||||||
|
|
||||||
<CheckBox AutomationProperties.Name="{Binding RelativeSource={RelativeSource Self}, Path=Content.(AccessText.Text), Converter={StaticResource AccessTextToTextConverter}}" IsChecked="{Binding Settings.IgnoreOrientation}">
|
|
||||||
<CheckBox.Content>
|
|
||||||
<AccessText Text="{x:Static p:Resources.Input_IgnoreOrientation}" TextWrapping="Wrap" />
|
|
||||||
</CheckBox.Content>
|
|
||||||
</CheckBox>
|
|
||||||
|
|
||||||
<CheckBox AutomationProperties.Name="{Binding RelativeSource={RelativeSource Self}, Path=Content.(AccessText.Text), Converter={StaticResource AccessTextToTextConverter}}" IsChecked="{Binding Settings.Replace}">
|
|
||||||
<CheckBox.Content>
|
|
||||||
<AccessText Text="{x:Static p:Resources.Input_Replace}" TextWrapping="Wrap" />
|
|
||||||
</CheckBox.Content>
|
|
||||||
</CheckBox>
|
|
||||||
|
|
||||||
<CheckBox AutomationProperties.Name="{Binding RelativeSource={RelativeSource Self}, Path=Content.(AccessText.Text), Converter={StaticResource AccessTextToTextConverter}}" IsChecked="{Binding Settings.RemoveMetadata}">
|
|
||||||
<CheckBox.Content>
|
|
||||||
<AccessText Text="{x:Static p:Resources.Input_RemoveMetadata}" TextWrapping="Wrap" />
|
|
||||||
</CheckBox.Content>
|
|
||||||
</CheckBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Separator line -->
|
|
||||||
<Border
|
|
||||||
Grid.Row="2"
|
|
||||||
Height="1"
|
|
||||||
Margin="0,8"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Background="{DynamicResource DividerStrokeColorDefaultBrush}" />
|
|
||||||
|
|
||||||
<ui:InfoBar
|
|
||||||
Grid.Row="3"
|
|
||||||
Margin="16,0"
|
|
||||||
IsClosable="False"
|
|
||||||
IsOpen="{Binding TryingToResizeGifFiles}"
|
|
||||||
Message="{x:Static p:Resources.Input_GifWarning}"
|
|
||||||
Severity="Warning" />
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
|
||||||
<Grid Grid.Row="4" Margin="16,8,16,16">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<ui:Button
|
|
||||||
Padding="0"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Open_settings}"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderBrush="Transparent"
|
|
||||||
Command="{Binding OpenSettingsCommand}"
|
|
||||||
ToolTipService.ToolTip="{x:Static p:Resources.Open_settings}">
|
|
||||||
<ui:Button.Content>
|
|
||||||
<ui:SymbolIcon FontSize="20" Symbol="Settings20" />
|
|
||||||
</ui:Button.Content>
|
|
||||||
</ui:Button>
|
|
||||||
|
|
||||||
<ui:Button
|
|
||||||
Grid.Column="1"
|
|
||||||
MinWidth="76"
|
|
||||||
Appearance="Primary"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.Resize_Tooltip}"
|
|
||||||
Command="{Binding ResizeCommand}"
|
|
||||||
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}" />
|
|
||||||
</StackPanel>
|
|
||||||
</ui:Button>
|
|
||||||
|
|
||||||
<ui:Button
|
|
||||||
Grid.Column="2"
|
|
||||||
MinWidth="76"
|
|
||||||
Margin="8,0,0,0"
|
|
||||||
Command="{Binding CancelCommand}"
|
|
||||||
Content="{x:Static p:Resources.Cancel}"
|
|
||||||
IsCancel="True" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information.
|
|
||||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
using ImageResizer.ViewModels;
|
|
||||||
using Wpf.Ui.Controls;
|
|
||||||
|
|
||||||
using static ImageResizer.ViewModels.InputViewModel;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public partial class InputPage : UserControl
|
|
||||||
{
|
|
||||||
public InputPage()
|
|
||||||
=> InitializeComponent();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pressing Enter key doesn't update value. PropertyChanged is only updated after losing focus to NumberBox.
|
|
||||||
/// We add this workaround the UI limitations and might need to be revisited or not needed anymore if we upgrade to WinUI3.
|
|
||||||
/// This function handles the KeyDown event for a NumberBox control.
|
|
||||||
/// It checks if the key pressed is 'Enter'.
|
|
||||||
/// According to the NumberBox name, it creates an instance of the KeyPressParams class with the appropriate dimension (Width or Height) and the parsed double value.
|
|
||||||
/// </summary>
|
|
||||||
private void Button_KeyDown(object sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
// Check if the key pressed is the 'Enter' key
|
|
||||||
if (e.Key == Key.Enter)
|
|
||||||
{
|
|
||||||
var numberBox = sender as NumberBox;
|
|
||||||
var viewModel = (InputViewModel)DataContext;
|
|
||||||
KeyPressParams keyParams;
|
|
||||||
if (double.TryParse(((System.Windows.Controls.TextBox)e.OriginalSource).Text, out double number))
|
|
||||||
{
|
|
||||||
// Determine which NumberBox triggered the event based on its name
|
|
||||||
switch (numberBox.Name)
|
|
||||||
{
|
|
||||||
case "WidthNumberBox":
|
|
||||||
keyParams = new KeyPressParams
|
|
||||||
{
|
|
||||||
Value = number,
|
|
||||||
Dimension = Dimension.Width,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "HeightNumberBox":
|
|
||||||
keyParams = new KeyPressParams
|
|
||||||
{
|
|
||||||
Value = number,
|
|
||||||
Dimension = Dimension.Height,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Return without EnterKeyPressedCommand executed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.EnterKeyPressedCommand.Execute(keyParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<ui:FluentWindow
|
|
||||||
x:Class="ImageResizer.Views.MainWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:local="clr-namespace:ImageResizer.Views"
|
|
||||||
xmlns:p="clr-namespace:ImageResizer.Properties"
|
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
|
||||||
xmlns:vm="clr-namespace:ImageResizer.ViewModels"
|
|
||||||
Name="_this"
|
|
||||||
Width="360"
|
|
||||||
Height="506"
|
|
||||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
|
||||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
|
||||||
AutomationProperties.Name="{x:Static p:Resources.ImageResizer}"
|
|
||||||
ExtendsContentIntoTitleBar="True"
|
|
||||||
Icon="/PowerToys.ImageResizer;component/Resources/ImageResizer.ico"
|
|
||||||
ResizeMode="NoResize"
|
|
||||||
SizeToContent="Height"
|
|
||||||
WindowCornerPreference="Default"
|
|
||||||
WindowStartupLocation="CenterScreen">
|
|
||||||
|
|
||||||
<Window.Resources>
|
|
||||||
<DataTemplate DataType="{x:Type vm:InputViewModel}">
|
|
||||||
<local:InputPage />
|
|
||||||
</DataTemplate>
|
|
||||||
<DataTemplate DataType="{x:Type vm:ProgressViewModel}">
|
|
||||||
<local:ProgressPage />
|
|
||||||
</DataTemplate>
|
|
||||||
<DataTemplate DataType="{x:Type vm:ResultsViewModel}">
|
|
||||||
<local:ResultsPage />
|
|
||||||
</DataTemplate>
|
|
||||||
</Window.Resources>
|
|
||||||
|
|
||||||
<Window.TaskbarItemInfo>
|
|
||||||
<TaskbarItemInfo ProgressState="Normal" ProgressValue="{Binding Progress}" />
|
|
||||||
</Window.TaskbarItemInfo>
|
|
||||||
|
|
||||||
<behaviors:Interaction.Triggers>
|
|
||||||
<behaviors:EventTrigger EventName="Loaded">
|
|
||||||
<behaviors:InvokeCommandAction Command="{Binding LoadCommand}" CommandParameter="{Binding ElementName=_this}" />
|
|
||||||
</behaviors:EventTrigger>
|
|
||||||
</behaviors:Interaction.Triggers>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<ui:TitleBar
|
|
||||||
x:Name="TitleBar"
|
|
||||||
Title="{x:Static p:Resources.ImageResizer}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Height="32"
|
|
||||||
Padding="16,0,16,0"
|
|
||||||
ShowMaximize="False"
|
|
||||||
ShowMinimize="False">
|
|
||||||
<ui:TitleBar.Icon>
|
|
||||||
<ui:ImageIcon Source="/PowerToys.ImageResizer;component/Resources/ImageResizer.ico" />
|
|
||||||
</ui:TitleBar.Icon>
|
|
||||||
</ui:TitleBar>
|
|
||||||
|
|
||||||
<ContentPresenter Grid.Row="1" Content="{Binding CurrentPage}" />
|
|
||||||
</Grid>
|
|
||||||
</ui:FluentWindow>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using ImageResizer.ViewModels;
|
|
||||||
using ManagedCommon;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using Wpf.Ui.Controls;
|
|
||||||
|
|
||||||
using AppResources = ImageResizer.Properties.Resources;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public partial class MainWindow : FluentWindow, IMainView
|
|
||||||
{
|
|
||||||
public MainWindow(MainViewModel viewModel)
|
|
||||||
{
|
|
||||||
DataContext = viewModel;
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
if (OSVersionHelper.IsWindows11())
|
|
||||||
{
|
|
||||||
WindowBackdropType = WindowBackdropType.Mica;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WindowBackdropType = WindowBackdropType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this, WindowBackdropType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> OpenPictureFiles()
|
|
||||||
{
|
|
||||||
var openFileDialog = new OpenFileDialog
|
|
||||||
{
|
|
||||||
Filter = AppResources.PictureFilter +
|
|
||||||
"|*.bmp;*.dib;*.exif;*.gif;*.jfif;*.jpe;*.jpeg;*.jpg;*.jxr;*.png;*.rle;*.tif;*.tiff;*.wdp|" +
|
|
||||||
AppResources.AllFilesFilter + "|*.*",
|
|
||||||
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
|
|
||||||
Multiselect = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (openFileDialog.ShowDialog() != true)
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return openFileDialog.FileNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IMainView.Close()
|
|
||||||
=> Dispatcher.Invoke((Action)Close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// 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.Globalization;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views;
|
|
||||||
|
|
||||||
public class NumberBoxValueConverter : IValueConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the underlying double value to a display-friendly format. Ensures that NaN values
|
|
||||||
/// are not propagated to the UI.
|
|
||||||
/// </summary>
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|
||||||
value is double d && double.IsNaN(d) ? 0 : value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts the user input back to the underlying double value. If the input is not a valid
|
|
||||||
/// number, 0 is returned.
|
|
||||||
/// </summary>
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|
||||||
value switch
|
|
||||||
{
|
|
||||||
null => 0,
|
|
||||||
double d when double.IsNaN(d) => 0,
|
|
||||||
string str when !double.TryParse(str, out _) => 0,
|
|
||||||
_ => value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
x:Class="ImageResizer.Views.ProgressPage"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:local="clr-namespace:ImageResizer.Views"
|
|
||||||
xmlns:p="clr-namespace:ImageResizer.Properties">
|
|
||||||
|
|
||||||
<UserControl.Resources>
|
|
||||||
<local:TimeRemainingConverter x:Key="TimeRemainingConverter" />
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<behaviors:Interaction.Triggers>
|
|
||||||
<behaviors:EventTrigger EventName="Loaded">
|
|
||||||
<behaviors:InvokeCommandAction Command="{Binding StartCommand}" />
|
|
||||||
</behaviors:EventTrigger>
|
|
||||||
</behaviors:Interaction.Triggers>
|
|
||||||
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
|
||||||
Margin="12,12,12,0"
|
|
||||||
FontSize="16"
|
|
||||||
Text="{x:Static p:Resources.Progress_MainInstruction}" />
|
|
||||||
<TextBlock
|
|
||||||
Margin="12,12,12,0"
|
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
|
||||||
Text="{Binding TimeRemaining, Converter={StaticResource TimeRemainingConverter}}" />
|
|
||||||
<ProgressBar
|
|
||||||
Height="16"
|
|
||||||
Margin="12,12,12,0"
|
|
||||||
Maximum="1"
|
|
||||||
Value="{Binding Progress}" />
|
|
||||||
<Border
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
Padding="12,12"
|
|
||||||
BorderThickness="0,1,0,0">
|
|
||||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
|
||||||
<Button
|
|
||||||
MinWidth="76"
|
|
||||||
Command="{Binding StopCommand}"
|
|
||||||
Content="{x:Static p:Resources.Progress_Stop}"
|
|
||||||
IsCancel="True" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public partial class ProgressPage : UserControl
|
|
||||||
{
|
|
||||||
public ProgressPage()
|
|
||||||
=> InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<UserControl
|
|
||||||
x:Class="ImageResizer.Views.ResultsPage"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:p="clr-namespace:ImageResizer.Properties">
|
|
||||||
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
|
||||||
Margin="12,12,12,0"
|
|
||||||
FontSize="16"
|
|
||||||
Text="{x:Static p:Resources.Results_MainInstruction}" />
|
|
||||||
<ScrollViewer HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
|
|
||||||
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="ResizeError">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,8,0,0"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Text="{Binding File}" />
|
|
||||||
<TextBlock Text="{Binding Error}" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<Border
|
|
||||||
Margin="0,12,0,0"
|
|
||||||
Padding="12,12"
|
|
||||||
Background="{DynamicResource LayerFillColorDefaultBrush}"
|
|
||||||
BorderBrush="{DynamicResource DividerStrokeColorDefaultBrush}"
|
|
||||||
BorderThickness="0,1,0,0">
|
|
||||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
|
||||||
<Button
|
|
||||||
MinWidth="76"
|
|
||||||
Command="{Binding CloseCommand}"
|
|
||||||
Content="{x:Static p:Resources.Results_Close}"
|
|
||||||
IsCancel="True"
|
|
||||||
IsDefault="True" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
public partial class ResultsPage : UserControl
|
|
||||||
{
|
|
||||||
public ResultsPage()
|
|
||||||
=> InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 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.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
using ImageResizer.Models;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views;
|
|
||||||
|
|
||||||
[ValueConversion(typeof(ResizeSize), typeof(string))]
|
|
||||||
public sealed partial class SizeTypeToHelpTextConverter : IValueConverter
|
|
||||||
{
|
|
||||||
private const char MultiplicationSign = '\u00D7';
|
|
||||||
|
|
||||||
private readonly EnumValueConverter _enumConverter = new();
|
|
||||||
private readonly AutoDoubleConverter _autoDoubleConverter = new();
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value is not ResizeSize size)
|
|
||||||
{
|
|
||||||
return DependencyProperty.UnsetValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string EnumToString(Enum value, string parameter = null) =>
|
|
||||||
_enumConverter.Convert(value, typeof(string), parameter, culture) as string;
|
|
||||||
|
|
||||||
string DoubleToString(double value) =>
|
|
||||||
_autoDoubleConverter.Convert(value, typeof(string), null, culture) as string;
|
|
||||||
|
|
||||||
var fit = EnumToString(size.Fit, "ThirdPersonSingular");
|
|
||||||
var width = DoubleToString(size.Width);
|
|
||||||
var unit = EnumToString(size.Unit);
|
|
||||||
|
|
||||||
return size.ShowHeight ?
|
|
||||||
$"{fit} {width} {MultiplicationSign} {DoubleToString(size.Height)} {unit}" :
|
|
||||||
$"{fit} {width} {unit}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// 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.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
using ImageResizer.Models;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(Visibility), typeof(ResizeSize))]
|
|
||||||
internal class SizeTypeToVisibilityConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
return value != null && value.GetType() == typeof(CustomSize) ? Visibility.Visible : (object)Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
return (Visibility)value == Visibility.Visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(TiffCompressOption), typeof(string))]
|
|
||||||
internal class TiffCompressOptionConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> Resources.ResourceManager.GetString(
|
|
||||||
"TiffCompressOption_" + Enum.GetName(typeof(TiffCompressOption), value),
|
|
||||||
culture);
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
using ImageResizer.Properties;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(TimeSpan), typeof(string))]
|
|
||||||
internal class TimeRemainingConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
var timeRemaining = (TimeSpan)value;
|
|
||||||
|
|
||||||
var builder = new StringBuilder("Progress_TimeRemaining_");
|
|
||||||
|
|
||||||
if (timeRemaining.Hours != 0)
|
|
||||||
{
|
|
||||||
builder.Append(timeRemaining.Hours == 1 ? "Hour" : "Hours");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeRemaining.Hours != 0 || timeRemaining.Minutes > 0)
|
|
||||||
{
|
|
||||||
builder.Append(timeRemaining.Minutes == 1 ? "Minute" : "Minutes");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeRemaining.Hours == 0)
|
|
||||||
{
|
|
||||||
builder.Append(timeRemaining.Seconds == 1 ? "Second" : "Seconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Format(
|
|
||||||
culture,
|
|
||||||
Resources.ResourceManager.GetString(builder.ToString(), culture),
|
|
||||||
timeRemaining.Hours,
|
|
||||||
timeRemaining.Minutes,
|
|
||||||
timeRemaining.Seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#pragma warning disable IDE0073
|
|
||||||
// Copyright (c) Brice Lambson
|
|
||||||
// The Brice Lambson licenses this file to you under the MIT license.
|
|
||||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
||||||
#pragma warning restore IDE0073
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views
|
|
||||||
{
|
|
||||||
[ValueConversion(typeof(Visibility), typeof(bool))]
|
|
||||||
internal class VisibilityBoolConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> (Visibility)value == Visibility.Visible;
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
=> (bool)value ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// 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.Globalization;
|
|
||||||
using Wpf.Ui.Controls;
|
|
||||||
|
|
||||||
namespace ImageResizer.Views;
|
|
||||||
|
|
||||||
public class ZeroToEmptyStringNumberFormatter : INumberFormatter, INumberParser
|
|
||||||
{
|
|
||||||
public string FormatDouble(double? value) => value switch
|
|
||||||
{
|
|
||||||
null => string.Empty,
|
|
||||||
0 => string.Empty,
|
|
||||||
_ => value.Value.ToString(CultureInfo.CurrentCulture),
|
|
||||||
};
|
|
||||||
|
|
||||||
public double? ParseDouble(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return double.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out double result) ? result : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FormatInt(int? value) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public string FormatUInt(uint? value) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public int? ParseInt(string value) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public uint? ParseUInt(string value) => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
21
src/modules/imageresizer/ui/app.manifest
Normal file
21
src/modules/imageresizer/ui/app.manifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="ImageResizerUI.app"/>
|
||||||
|
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<!-- The combination of below two tags have the following effect:
|
||||||
|
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||||
|
2) System < Windows 10 Anniversary Update
|
||||||
|
-->
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
Reference in New Issue
Block a user