Compare commits

...

7 Commits

Author SHA1 Message Date
Leilei Zhang
3e67ae1bd8 fix comments 2025-12-26 11:19:50 +08:00
Leilei Zhang
f58f4dc5ad update string 2025-12-25 13:48:54 +08:00
Leilei Zhang
e4dda98b6e remove wrong file 2025-12-25 12:31:20 +08:00
Leilei Zhang
b61231b3dc add localization 2025-12-25 12:16:30 +08:00
Leilei Zhang
aac813cb71 fix imageresizer UT 2025-12-15 14:34:16 +08:00
Leilei Zhang
02fa0daea5 Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/imagecli 2025-12-15 10:25:48 +08:00
Leilei Zhang (from Dev Box)
dcfb93b26d Add standard CLI support for Image Resizer 2025-12-15 09:53:08 +08:00
35 changed files with 2385 additions and 32 deletions

View File

@@ -131,6 +131,8 @@
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
"PowerToys.ImageResizerExt.dll",
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",

View File

@@ -442,6 +442,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">

View File

@@ -0,0 +1,93 @@
# CLI Conventions
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
## Library
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
```xml
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
```
Add the reference to your project:
```xml
<PackageReference Include="System.CommandLine" />
```
## Option Naming and Definition
- Use `--kebab-case` for long form (e.g., `--shrink-only`).
- Use single `-x` for short form (e.g., `-s`, `-w`).
- Define aliases as static readonly arrays: `["--silent", "-s"]`.
- Create options using `Option<T>` with descriptive help text.
- Add validators for options that require range or format checking.
## RootCommand Setup
- Create a `RootCommand` with a brief description.
- Add all options and arguments to the command.
## Parsing
- Use `Parser(rootCommand).Parse(args)` to parse CLI arguments.
- Extract option values using `parseResult.GetValueForOption()`.
- Note: Use `Parser` directly; `RootCommand.Parse()` may not be available with the pinned System.CommandLine version.
### Parse/Validation Errors
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
## Examples
Reference implementations:
- Awake: `src/modules/Awake/Awake/Program.cs`
- ImageResizer: `src/modules/imageresizer/ui/Cli/`
## Help Output
- Provide a `PrintUsage()` method for custom help formatting if needed.
## Best Practices
1. **Consistency**: Follow existing module patterns.
2. **Documentation**: Always provide help text for each option.
3. **Validation**: Validate input and provide clear error messages.
4. **Atomicity**: Make one logical change per PR; avoid drive-by refactors.
5. **Build/Test Discipline**: Build and test synchronously, one terminal per operation.
6. **Style**: Follow repo analyzers (`.editorconfig`, StyleCop) and formatting rules.
## Logging Requirements
- Use `ManagedCommon.Logger` for consistent logging.
- Initialize logging early in `Main()`.
- Use dual output (console + log file) for errors and warnings to ensure visibility.
- Reference: `src/modules/imageresizer/ui/Cli/CliLogger.cs`
## Error Handling
### Exit Codes
- `0`: Success
- `1`: General error (parsing, validation, runtime)
- `2`: Invalid arguments (optional)
### Exception Handling
- Always wrap `Main()` in try-catch for unhandled exceptions.
- Log exceptions before exiting with non-zero code.
- Display user-friendly error messages to stderr.
- Preserve detailed stack traces in log files only.
## Testing Requirements
- Include tests for argument parsing, validation, and edge cases.
- Place CLI tests in module-specific test projects (e.g., `src/modules/[module]/tests/*CliTests.cs`).
## Signing and Deployment
- CLI executables are signed automatically in CI/CD.
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
- Use self-contained deployment (import `Common.SelfContained.props`).

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.ImageResizerCLI</AssemblyTitle>
<AssemblyDescription>PowerToys Image Resizer Command Line Interface</AssemblyDescription>
<Description>PowerToys Image Resizer CLI</Description>
<OutputType>Exe</OutputType>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<AssemblyName>PowerToys.ImageResizerCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
// 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.Text;
using ImageResizer.Cli;
using ManagedCommon;
namespace ImageResizerCLI;
internal static class Program
{
private static int Main(string[] args)
{
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException)
{
// Ignore invalid culture and fall back to default.
}
Console.InputEncoding = Encoding.Unicode;
// Initialize logger to file (same as other modules)
CliLogger.Initialize("\\ImageResizer\\Logs");
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
try
{
var executor = new ImageResizerCliExecutor();
return executor.Run(args);
}
catch (Exception ex)
{
CliLogger.Error($"Unhandled exception: {ex.Message}");
CliLogger.Error($"Stack trace: {ex.StackTrace}");
Console.Error.WriteLine($"Fatal error: {ex.Message}");
return 1;
}
}
}

View File

@@ -0,0 +1,320 @@
// 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.Cli;
using ImageResizer.Models;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Cli
{
[TestClass]
public class CliSettingsApplierTests
{
private Settings CreateDefaultSettings()
{
var settings = new Settings();
settings.Sizes.Add(new ResizeSize(0, "Small", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(1, "Medium", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel));
settings.Sizes.Add(new ResizeSize(2, "Large", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel));
return settings;
}
[TestMethod]
public void Apply_WithCustomWidth_SetsCustomSizeWidth()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithCustomHeight_SetsCustomSizeHeight()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithCustomSize_SelectsCustomSizeIndex()
{
var options = new CliOptions { Width = 800, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size index should be settings.Sizes.Count
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithZeroWidth_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 0, Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(0.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithZeroHeight_SetsZeroForAutoCalculation()
{
var options = new CliOptions { Width = 800, Height = 0 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(0.0, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithNullWidthAndHeight_DoesNotModifyCustomSize()
{
var options = new CliOptions { Width = null, Height = null };
var settings = CreateDefaultSettings();
var originalWidth = settings.CustomSize.Width;
var originalHeight = settings.CustomSize.Height;
CliSettingsApplier.Apply(options, settings);
// When both null, should not modify CustomSize (keeps default 1024x640)
Assert.AreEqual(originalWidth, settings.CustomSize.Width);
Assert.AreEqual(originalHeight, settings.CustomSize.Height);
}
[TestMethod]
public void Apply_WithUnit_SetsCustomSizeUnit()
{
var options = new CliOptions { Width = 100, Unit = ResizeUnit.Percent };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
}
[TestMethod]
public void Apply_WithFit_SetsCustomSizeFit()
{
var options = new CliOptions { Width = 800, Fit = ResizeFit.Fill };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
}
[TestMethod]
public void Apply_WithValidSizeIndex_SetsSelectedSizeIndex()
{
var options = new CliOptions { SizeIndex = 1 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(1, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithInvalidSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = 99 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
// Should remain unchanged when invalid
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithNegativeSizeIndex_DoesNotChangeSelection()
{
var options = new CliOptions { SizeIndex = -1 };
var settings = CreateDefaultSettings();
var originalIndex = settings.SelectedSizeIndex;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalIndex, settings.SelectedSizeIndex);
}
[TestMethod]
public void Apply_WithShrinkOnly_SetsShrinkOnly()
{
var options = new CliOptions { ShrinkOnly = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.ShrinkOnly);
}
[TestMethod]
public void Apply_WithReplace_SetsReplace()
{
var options = new CliOptions { Replace = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.Replace);
}
[TestMethod]
public void Apply_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var options = new CliOptions { IgnoreOrientation = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.IgnoreOrientation);
}
[TestMethod]
public void Apply_WithRemoveMetadata_SetsRemoveMetadata()
{
var options = new CliOptions { RemoveMetadata = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.RemoveMetadata);
}
[TestMethod]
public void Apply_WithJpegQualityLevel_SetsJpegQualityLevel()
{
var options = new CliOptions { JpegQualityLevel = 85 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(85, settings.JpegQualityLevel);
}
[TestMethod]
public void Apply_WithKeepDateModified_SetsKeepDateModified()
{
var options = new CliOptions { KeepDateModified = true };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.IsTrue(settings.KeepDateModified);
}
[TestMethod]
public void Apply_WithFileName_SetsFileName()
{
var options = new CliOptions { FileName = "%1 (%2)" };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual("%1 (%2)", settings.FileName);
}
[TestMethod]
public void Apply_WithEmptyFileName_DoesNotChangeFileName()
{
var options = new CliOptions { FileName = string.Empty };
var settings = CreateDefaultSettings();
var originalFileName = settings.FileName;
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(originalFileName, settings.FileName);
}
[TestMethod]
public void Apply_WithMultipleOptions_AppliesAllOptions()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
Unit = ResizeUnit.Percent,
Fit = ResizeFit.Fill,
ShrinkOnly = true,
Replace = true,
IgnoreOrientation = true,
RemoveMetadata = true,
JpegQualityLevel = 90,
KeepDateModified = true,
FileName = "test_%2",
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(800.0, settings.CustomSize.Width);
Assert.AreEqual(600.0, settings.CustomSize.Height);
Assert.AreEqual(ResizeUnit.Percent, settings.CustomSize.Unit);
Assert.AreEqual(ResizeFit.Fill, settings.CustomSize.Fit);
Assert.IsTrue(settings.ShrinkOnly);
Assert.IsTrue(settings.Replace);
Assert.IsTrue(settings.IgnoreOrientation);
Assert.IsTrue(settings.RemoveMetadata);
Assert.AreEqual(90, settings.JpegQualityLevel);
Assert.IsTrue(settings.KeepDateModified);
Assert.AreEqual("test_%2", settings.FileName);
}
[TestMethod]
public void Apply_CustomSizeTakesPrecedence_OverSizeIndex()
{
var options = new CliOptions
{
Width = 800,
Height = 600,
SizeIndex = 1, // Should be ignored when Width/Height specified
};
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
// Custom size should be selected, not preset
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyWidth_StillSelectsCustomSize()
{
var options = new CliOptions { Width = 800 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(800.0, settings.CustomSize.Width);
}
[TestMethod]
public void Apply_WithOnlyHeight_StillSelectsCustomSize()
{
var options = new CliOptions { Height = 600 };
var settings = CreateDefaultSettings();
CliSettingsApplier.Apply(options, settings);
Assert.AreEqual(settings.Sizes.Count, settings.SelectedSizeIndex);
Assert.AreEqual(600.0, settings.CustomSize.Height);
}
}
}

View File

@@ -0,0 +1,268 @@
// 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 ImageResizer.Cli.Commands;
using ImageResizer.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Tests.Models
{
[TestClass]
public class CliOptionsTests
{
private static readonly string[] _multiFileArgs = new[] { "test1.jpg", "test2.jpg", "test3.jpg" };
private static readonly string[] _mixedOptionsArgs = new[] { "--width", "800", "test1.jpg", "--height", "600", "test2.jpg" };
[TestMethod]
public void Parse_WithValidWidth_SetsWidth()
{
var args = new[] { "--width", "800", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
}
[TestMethod]
public void Parse_WithValidHeight_SetsHeight()
{
var args = new[] { "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithShortWidthAlias_WorksIdentically()
{
var longFormArgs = new[] { "--width", "800", "test.jpg" };
var shortFormArgs = new[] { "-w", "800", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Width, shortForm.Width);
}
[TestMethod]
public void Parse_WithShortHeightAlias_WorksIdentically()
{
var longFormArgs = new[] { "--height", "600", "test.jpg" };
var shortFormArgs = new[] { "-h", "600", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.Height, shortForm.Height);
}
[TestMethod]
public void Parse_WithValidUnit_SetsUnit()
{
var args = new[] { "--unit", "Percent", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Percent, options.Unit);
}
[TestMethod]
public void Parse_WithValidFit_SetsFit()
{
var args = new[] { "--fit", "Fill", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeFit.Fill, options.Fit);
}
[TestMethod]
public void Parse_WithSizeIndex_SetsSizeIndex()
{
var args = new[] { "--size", "2", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(2, options.SizeIndex);
}
[TestMethod]
public void Parse_WithShrinkOnly_SetsShrinkOnly()
{
var args = new[] { "--shrink-only", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ShrinkOnly);
}
[TestMethod]
public void Parse_WithReplace_SetsReplace()
{
var args = new[] { "--replace", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.Replace);
}
[TestMethod]
public void Parse_WithIgnoreOrientation_SetsIgnoreOrientation()
{
var args = new[] { "--ignore-orientation", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.IgnoreOrientation);
}
[TestMethod]
public void Parse_WithRemoveMetadata_SetsRemoveMetadata()
{
var args = new[] { "--remove-metadata", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.RemoveMetadata);
}
[TestMethod]
public void Parse_WithValidQuality_SetsQuality()
{
var args = new[] { "--quality", "85", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(85, options.JpegQualityLevel);
}
[TestMethod]
public void Parse_WithKeepDateModified_SetsKeepDateModified()
{
var args = new[] { "--keep-date-modified", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.KeepDateModified);
}
[TestMethod]
public void Parse_WithFileName_SetsFileName()
{
var args = new[] { "--filename", "%1 (%2)", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("%1 (%2)", options.FileName);
}
[TestMethod]
public void Parse_WithDestination_SetsDestinationDirectory()
{
var args = new[] { "--destination", "C:\\Output", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual("C:\\Output", options.DestinationDirectory);
}
[TestMethod]
public void Parse_WithShortDestinationAlias_WorksIdentically()
{
var longFormArgs = new[] { "--destination", "C:\\Output", "test.jpg" };
var shortFormArgs = new[] { "-d", "C:\\Output", "test.jpg" };
var longForm = CliOptions.Parse(longFormArgs);
var shortForm = CliOptions.Parse(shortFormArgs);
Assert.AreEqual(longForm.DestinationDirectory, shortForm.DestinationDirectory);
}
[TestMethod]
public void Parse_WithProgressLines_SetsProgressLines()
{
var args = new[] { "--progress-lines", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithAccessibleAlias_SetsProgressLines()
{
var args = new[] { "--accessible", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(true, options.ProgressLines);
}
[TestMethod]
public void Parse_WithMultipleFiles_AddsAllFiles()
{
var args = _multiFileArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(3, options.Files.Count);
CollectionAssert.Contains(options.Files.ToList(), "test1.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test2.jpg");
CollectionAssert.Contains(options.Files.ToList(), "test3.jpg");
}
[TestMethod]
public void Parse_WithMixedOptionsAndFiles_ParsesCorrectly()
{
var args = _mixedOptionsArgs;
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(600.0, options.Height);
Assert.AreEqual(2, options.Files.Count);
}
[TestMethod]
public void Parse_WithHelp_SetsShowHelp()
{
var args = new[] { "--help" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowHelp);
}
[TestMethod]
public void Parse_WithShowConfig_SetsShowConfig()
{
var args = new[] { "--show-config" };
var options = CliOptions.Parse(args);
Assert.IsTrue(options.ShowConfig);
}
[TestMethod]
public void Parse_WithNoArguments_ReturnsEmptyOptions()
{
var args = Array.Empty<string>();
var options = CliOptions.Parse(args);
Assert.IsNotNull(options);
Assert.AreEqual(0, options.Files.Count);
}
[TestMethod]
public void Parse_WithZeroWidth_AllowsZeroValue()
{
var args = new[] { "--width", "0", "--height", "600", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(0.0, options.Width);
Assert.AreEqual(600.0, options.Height);
}
[TestMethod]
public void Parse_WithZeroHeight_AllowsZeroValue()
{
var args = new[] { "--width", "800", "--height", "0", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(800.0, options.Width);
Assert.AreEqual(0.0, options.Height);
}
[TestMethod]
public void Parse_CaseInsensitiveEnums_ParsesCorrectly()
{
var args = new[] { "--unit", "pixel", "--fit", "fit", "test.jpg" };
var options = CliOptions.Parse(args);
Assert.AreEqual(ResizeUnit.Pixel, options.Unit);
Assert.AreEqual(ResizeFit.Fit, options.Fit);
}
}
}

View File

@@ -25,20 +25,27 @@ namespace ImageResizer.Models
[TestMethod]
public void FromCommandLineWorks()
{
// Use actual test files that exist in the test directory
var testDir = Path.GetDirectoryName(typeof(ResizeBatchTests).Assembly.Location);
var file1 = Path.Combine(testDir, "Test.jpg");
var file2 = Path.Combine(testDir, "Test.png");
var file3 = Path.Combine(testDir, "Test.gif");
var standardInput =
"Image1.jpg" + EOL +
"Image2.jpg";
file1 + EOL +
file2;
var args = new[]
{
"/d", "OutputDir",
"Image3.jpg",
file3,
};
var result = ResizeBatch.FromCommandLine(
new StringReader(standardInput),
args);
CollectionAssert.AreEquivalent(new List<string> { "Image1.jpg", "Image2.jpg", "Image3.jpg" }, result.Files.ToArray());
var files = result.Files.Select(Path.GetFileName).ToArray();
CollectionAssert.AreEquivalent(new List<string> { "Test.jpg", "Test.png", "Test.gif" }, files);
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}

View File

@@ -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 ManagedCommon;
namespace ImageResizer.Cli
{
public static class CliLogger
{
private static bool _initialized;
public static void Initialize(string logSubFolder)
{
if (!_initialized)
{
Logger.InitializeLogger(logSubFolder);
_initialized = true;
}
}
public static void Info(string message) => Logger.LogInfo(message);
public static void Warn(string message) => Logger.LogWarning(message);
public static void Error(string message) => Logger.LogError(message);
}
}

View File

@@ -0,0 +1,122 @@
// 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 ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Applies CLI options to Settings object.
/// Separated from executor logic for Single Responsibility Principle.
/// </summary>
public static class CliSettingsApplier
{
/// <summary>
/// Applies CLI options to the settings, overriding default values.
/// </summary>
/// <param name="cliOptions">The CLI options to apply.</param>
/// <param name="settings">The settings to modify.</param>
public static void Apply(CliOptions cliOptions, Settings settings)
{
// Handle complex size options first
ApplySizeOptions(cliOptions, settings);
// Apply simple property mappings
ApplySimpleOptions(cliOptions, settings);
}
private static void ApplySizeOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.Width.HasValue || cliOptions.Height.HasValue)
{
ApplyCustomSizeOptions(cliOptions, settings);
}
else if (cliOptions.SizeIndex.HasValue)
{
ApplyPresetSizeOption(cliOptions, settings);
}
}
private static void ApplyCustomSizeOptions(CliOptions cliOptions, Settings settings)
{
// Set dimensions (0 = auto-calculate for aspect ratio preservation)
// Implementation: ResizeSize.ConvertToPixels() returns double.PositiveInfinity for 0 in Fit mode,
// causing Math.Min(scaleX, scaleY) to preserve aspect ratio by selecting the non-zero scale.
// For Fill/Stretch modes, 0 uses the original dimension instead.
settings.CustomSize.Width = cliOptions.Width ?? 0;
settings.CustomSize.Height = cliOptions.Height ?? 0;
// Apply optional properties
if (cliOptions.Unit.HasValue)
{
settings.CustomSize.Unit = cliOptions.Unit.Value;
}
if (cliOptions.Fit.HasValue)
{
settings.CustomSize.Fit = cliOptions.Fit.Value;
}
// Select custom size (index = Sizes.Count)
settings.SelectedSizeIndex = settings.Sizes.Count;
}
private static void ApplyPresetSizeOption(CliOptions cliOptions, Settings settings)
{
var index = cliOptions.SizeIndex.Value;
if (index >= 0 && index < settings.Sizes.Count)
{
settings.SelectedSizeIndex = index;
}
else
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_WarningInvalidSizeIndex, index));
CliLogger.Warn($"Invalid size index: {index}");
}
}
private static void ApplySimpleOptions(CliOptions cliOptions, Settings settings)
{
if (cliOptions.ShrinkOnly.HasValue)
{
settings.ShrinkOnly = cliOptions.ShrinkOnly.Value;
}
if (cliOptions.Replace.HasValue)
{
settings.Replace = cliOptions.Replace.Value;
}
if (cliOptions.IgnoreOrientation.HasValue)
{
settings.IgnoreOrientation = cliOptions.IgnoreOrientation.Value;
}
if (cliOptions.RemoveMetadata.HasValue)
{
settings.RemoveMetadata = cliOptions.RemoveMetadata.Value;
}
if (cliOptions.JpegQualityLevel.HasValue)
{
settings.JpegQualityLevel = cliOptions.JpegQualityLevel.Value;
}
if (cliOptions.KeepDateModified.HasValue)
{
settings.KeepDateModified = cliOptions.KeepDateModified.Value;
}
if (!string.IsNullOrEmpty(cliOptions.FileName))
{
settings.FileName = cliOptions.FileName;
}
}
}
}

View File

@@ -0,0 +1,90 @@
// 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.CommandLine;
using ImageResizer.Cli.Options;
namespace ImageResizer.Cli.Commands
{
/// <summary>
/// Root command for the ImageResizer CLI.
/// </summary>
public sealed class ImageResizerRootCommand : RootCommand
{
public ImageResizerRootCommand()
: base("PowerToys Image Resizer - Resize images from command line")
{
HelpOption = new HelpOption();
ShowConfigOption = new ShowConfigOption();
DestinationOption = new DestinationOption();
WidthOption = new WidthOption();
HeightOption = new HeightOption();
UnitOption = new UnitOption();
FitOption = new FitOption();
SizeOption = new SizeOption();
ShrinkOnlyOption = new ShrinkOnlyOption();
ReplaceOption = new ReplaceOption();
IgnoreOrientationOption = new IgnoreOrientationOption();
RemoveMetadataOption = new RemoveMetadataOption();
QualityOption = new QualityOption();
KeepDateModifiedOption = new KeepDateModifiedOption();
FileNameOption = new FileNameOption();
ProgressLinesOption = new ProgressLinesOption();
FilesArgument = new FilesArgument();
AddOption(HelpOption);
AddOption(ShowConfigOption);
AddOption(DestinationOption);
AddOption(WidthOption);
AddOption(HeightOption);
AddOption(UnitOption);
AddOption(FitOption);
AddOption(SizeOption);
AddOption(ShrinkOnlyOption);
AddOption(ReplaceOption);
AddOption(IgnoreOrientationOption);
AddOption(RemoveMetadataOption);
AddOption(QualityOption);
AddOption(KeepDateModifiedOption);
AddOption(FileNameOption);
AddOption(ProgressLinesOption);
AddArgument(FilesArgument);
}
public HelpOption HelpOption { get; }
public ShowConfigOption ShowConfigOption { get; }
public DestinationOption DestinationOption { get; }
public WidthOption WidthOption { get; }
public HeightOption HeightOption { get; }
public UnitOption UnitOption { get; }
public FitOption FitOption { get; }
public SizeOption SizeOption { get; }
public ShrinkOnlyOption ShrinkOnlyOption { get; }
public ReplaceOption ReplaceOption { get; }
public IgnoreOrientationOption IgnoreOrientationOption { get; }
public RemoveMetadataOption RemoveMetadataOption { get; }
public QualityOption QualityOption { get; }
public KeepDateModifiedOption KeepDateModifiedOption { get; }
public FileNameOption FileNameOption { get; }
public ProgressLinesOption ProgressLinesOption { get; }
public FilesArgument FilesArgument { get; }
}
}

View File

@@ -0,0 +1,124 @@
// 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.Linq;
using System.Threading;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.Cli
{
/// <summary>
/// Executes Image Resizer CLI operations.
/// Instance-based design for better testability and Single Responsibility Principle.
/// </summary>
public class ImageResizerCliExecutor
{
/// <summary>
/// Runs the CLI executor with the provided command-line arguments.
/// </summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>Exit code.</returns>
public int Run(string[] args)
{
var cliOptions = CliOptions.Parse(args);
if (cliOptions.ParseErrors.Count > 0)
{
foreach (var error in cliOptions.ParseErrors)
{
Console.Error.WriteLine(error);
CliLogger.Error($"Parse error: {error}");
}
CliOptions.PrintUsage();
return 1;
}
if (cliOptions.ShowHelp)
{
CliOptions.PrintUsage();
return 0;
}
if (cliOptions.ShowConfig)
{
CliOptions.PrintConfig(Settings.Default);
return 0;
}
if (cliOptions.Files.Count == 0 && string.IsNullOrEmpty(cliOptions.PipeName))
{
Console.WriteLine(Resources.CLI_NoInputFiles);
CliOptions.PrintUsage();
return 1;
}
return RunSilentMode(cliOptions);
}
private int RunSilentMode(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
CliSettingsApplier.Apply(cliOptions, settings);
CliLogger.Info($"CLI mode: processing {batch.Files.Count} files");
// Use accessible line-based progress if requested or detected
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
var errors = batch.Process(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);
if (useLineBasedProgress)
{
// Milestone-based progress (0%, 25%, 50%, 75%, 100%)
int milestone = (progress / 25) * 25;
if (milestone > lastReportedMilestone || completed == (int)total)
{
lastReportedMilestone = milestone;
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total));
}
}
else
{
// Traditional carriage return mode
Console.Write(string.Format(CultureInfo.InvariantCulture, "\r{0}", string.Format(CultureInfo.InvariantCulture, Resources.CLI_ProgressFormat, progress, completed, (int)total)));
}
},
settings,
CancellationToken.None);
if (!useLineBasedProgress)
{
Console.WriteLine();
}
var errorList = errors.ToList();
if (errorList.Count > 0)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, Resources.CLI_CompletedWithErrors, errorList.Count));
CliLogger.Error($"Processing completed with {errorList.Count} error(s)");
foreach (var error in errorList)
{
Console.Error.WriteLine(string.Format(CultureInfo.InvariantCulture, " {0}: {1}", error.File, error.Error));
CliLogger.Error($" {error.File}: {error.Error}");
}
return 1;
}
CliLogger.Info("CLI batch completed successfully");
Console.WriteLine(Resources.CLI_AllFilesProcessed);
return 0;
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class DestinationOption : Option<string>
{
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FileNameOption : Option<string>
{
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
{
}
}
}

View File

@@ -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.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FilesArgument : Argument<string[]>
{
public FilesArgument()
: base("files", Properties.Resources.CLI_Option_Files)
{
Arity = ArgumentArity.ZeroOrMore;
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class FitOption : Option<ImageResizer.Models.ResizeFit?>
{
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HeightOption : Option<double?>
{
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class HelpOption : Option<bool>
{
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class IgnoreOrientationOption : Option<bool>
{
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class KeepDateModifiedOption : Option<bool>
{
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ProgressLinesOption : Option<bool>
{
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}
}

View File

@@ -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.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class QualityOption : Option<int?>
{
private static readonly string[] _aliases = ["--quality", "-q"];
public QualityOption()
: base(_aliases, Properties.Resources.CLI_Option_Quality)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && (value.Value < 1 || value.Value > 100))
{
result.ErrorMessage = "JPEG quality must be between 1 and 100.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class RemoveMetadataOption : Option<bool>
{
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ReplaceOption : Option<bool>
{
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShowConfigOption : Option<bool>
{
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class ShrinkOnlyOption : Option<bool>
{
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}
}

View File

@@ -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.
using System.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class SizeOption : Option<int?>
{
private static readonly string[] _aliases = ["--size"];
public SizeOption()
: base(_aliases, Properties.Resources.CLI_Option_Size)
{
AddValidator(result =>
{
var value = result.GetValueOrDefault<int?>();
if (value.HasValue && value.Value < 0)
{
result.ErrorMessage = "Size index must be a non-negative integer.";
}
});
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class UnitOption : Option<ImageResizer.Models.ResizeUnit?>
{
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
{
}
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandLine;
namespace ImageResizer.Cli.Options
{
public sealed class WidthOption : Option<double?>
{
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
{
}
}
}

View File

@@ -20,6 +20,7 @@
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA1863</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -51,6 +52,7 @@
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WPF-UI" />
</ItemGroup>

View File

@@ -0,0 +1,261 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.CommandLine.Parsing;
using System.Globalization;
using ImageResizer.Cli.Commands;
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
namespace ImageResizer.Models
{
/// <summary>
/// Represents the command-line options for ImageResizer CLI mode.
/// </summary>
public class CliOptions
{
/// <summary>
/// Gets or sets a value indicating whether to show help information.
/// </summary>
public bool ShowHelp { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show current configuration.
/// </summary>
public bool ShowConfig { get; set; }
/// <summary>
/// Gets or sets the destination directory for resized images.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// Gets or sets the width of the resized image.
/// </summary>
public double? Width { get; set; }
/// <summary>
/// Gets or sets the height of the resized image.
/// </summary>
public double? Height { get; set; }
/// <summary>
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
/// </summary>
public ResizeUnit? Unit { get; set; }
/// <summary>
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
/// </summary>
public ResizeFit? Fit { get; set; }
/// <summary>
/// Gets or sets the index of the preset size to use.
/// </summary>
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; }
/// <summary>
/// Gets or sets a value indicating whether to replace the original file.
/// </summary>
public bool? Replace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore orientation when resizing.
/// </summary>
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; }
/// <summary>
/// Gets or sets the JPEG quality level (1-100).
/// </summary>
public int? JpegQualityLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to keep the date modified.
/// </summary>
public bool? KeepDateModified { get; set; }
/// <summary>
/// Gets or sets the output filename format.
/// </summary>
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; }
/// <summary>
/// Gets the list of files to process.
/// </summary>
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; }
/// <summary>
/// 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;
/// <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)
{
var options = new CliOptions();
var cmd = new ImageResizerRootCommand();
// Parse using System.CommandLine
var parseResult = new Parser(cmd).Parse(args);
if (parseResult.Errors.Count > 0)
{
var errors = new List<string>(parseResult.Errors.Count);
foreach (var error in parseResult.Errors)
{
errors.Add(error.Message);
}
options.ParseErrors = new ReadOnlyCollection<string>(errors);
}
// Extract values from parse result using strongly typed options
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
options.Width = parseResult.GetValueForOption(cmd.WidthOption);
options.Height = parseResult.GetValueForOption(cmd.HeightOption);
options.Unit = parseResult.GetValueForOption(cmd.UnitOption);
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
// Convert bool to nullable bool (true -> true, false -> null)
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
options.RemoveMetadata = ToBoolOrNull(parseResult.GetValueForOption(cmd.RemoveMetadataOption));
options.KeepDateModified = ToBoolOrNull(parseResult.GetValueForOption(cmd.KeepDateModifiedOption));
options.ProgressLines = ToBoolOrNull(parseResult.GetValueForOption(cmd.ProgressLinesOption));
options.JpegQualityLevel = parseResult.GetValueForOption(cmd.QualityOption);
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
// Get files from arguments
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
if (files != null)
{
const string pipeNamePrefix = "\\\\.\\pipe\\";
foreach (var file in files)
{
// Check for pipe name (must be at the start of the path)
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
{
options.PipeName = file.Substring(pipeNamePrefix.Length);
}
else
{
options.Files.Add(file);
}
}
}
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)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.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, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
for (int i = 0; i < settings.Sizes.Count; i++)
{
var size = settings.Sizes[i];
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));
}
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));
}
}
/// <summary>
/// Prints usage information to the console.
/// </summary>
public static void PrintUsage()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
Console.WriteLine();
var cmd = new ImageResizerRootCommand();
// Print usage line
Console.WriteLine(Properties.Resources.CLI_UsageLine);
Console.WriteLine();
// Print options from the command definition
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
var description = option.Description ?? string.Empty;
Console.WriteLine($" {aliases,-30} {description}");
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -39,44 +40,78 @@ namespace ImageResizer.Models
_aiSuperResolutionService = null;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
/// <summary>
/// Validates if a file path is a supported image format.
/// </summary>
/// <param name="path">The file path to validate.</param>
/// <returns>True if the path is valid and points to a supported image file.</returns>
private static bool IsValidImagePath(string path)
{
var batch = new ResizeBatch();
const string pipeNamePrefix = "\\\\.\\pipe\\";
string pipeName = null;
for (var i = 0; i < args?.Length; i++)
if (string.IsNullOrWhiteSpace(path))
{
if (args[i] == "/d")
{
batch.DestinationDirectory = args[++i];
continue;
}
else if (args[i].Contains(pipeNamePrefix))
{
pipeName = args[i].Substring(pipeNamePrefix.Length);
continue;
}
batch.Files.Add(args[i]);
return false;
}
if (string.IsNullOrEmpty(pipeName))
if (!File.Exists(path))
{
return false;
}
var ext = Path.GetExtension(path)?.ToLowerInvariant();
var validExtensions = new[]
{
".bmp", ".dib", ".gif", ".jfif", ".jpe", ".jpeg", ".jpg",
".jxr", ".png", ".rle", ".tif", ".tiff", ".wdp",
};
return validExtensions.Contains(ext);
}
/// <summary>
/// Creates a ResizeBatch from CliOptions.
/// </summary>
/// <param name="standardInput">Standard input stream for reading additional file paths.</param>
/// <param name="options">The parsed CLI options.</param>
/// <returns>A ResizeBatch instance.</returns>
public static ResizeBatch FromCliOptions(TextReader standardInput, CliOptions options)
{
var batch = new ResizeBatch
{
DestinationDirectory = options.DestinationDirectory,
};
foreach (var file in options.Files)
{
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
if (string.IsNullOrEmpty(options.PipeName))
{
// NB: We read these from stdin since there are limits on the number of args you can have
// Only read from stdin if it's redirected (piped input), not from interactive terminal
string file;
if (standardInput != null)
if (standardInput != null && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
{
while ((file = standardInput.ReadLine()) != null)
{
batch.Files.Add(file);
// Convert relative paths to absolute paths
var absolutePath = Path.IsPathRooted(file) ? file : Path.GetFullPath(file);
if (IsValidImagePath(absolutePath))
{
batch.Files.Add(absolutePath);
}
}
}
}
else
{
using (NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", pipeName, PipeDirection.In))
new NamedPipeClientStream(".", options.PipeName, PipeDirection.In))
{
// Connect to the pipe or wait until the pipe is available.
pipeClient.Connect();
@@ -88,7 +123,10 @@ namespace ImageResizer.Models
// Display the read text to the console
while ((file = sr.ReadLine()) != null)
{
batch.Files.Add(file);
if (IsValidImagePath(file))
{
batch.Files.Add(file);
}
}
}
}
@@ -97,17 +135,26 @@ namespace ImageResizer.Models
return batch;
}
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
{
var options = CliOptions.Parse(args);
return FromCliOptions(standardInput, options);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
{
// 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.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
return Process(reportProgress, Settings.Default, cancellationToken);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
int completed = 0;
var errors = new ConcurrentBag<ResizeError>();
// 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.
// This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch.
var settings = Settings.Default;
// TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async
// APIs and a custom SynchronizationContext
Parallel.ForEach(

View File

@@ -716,5 +716,437 @@ namespace ImageResizer.Properties {
return ResourceManager.GetString("Width", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Processing {0} files....
/// </summary>
public static string CLI_ProcessingFiles {
get {
return ResourceManager.GetString("CLI_ProcessingFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to [{0}%] {1}/{2} completed.
/// </summary>
public static string CLI_ProgressFormat {
get {
return ResourceManager.GetString("CLI_ProgressFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completed with {0} error(s)..
/// </summary>
public static string CLI_CompletedWithErrors {
get {
return ResourceManager.GetString("CLI_CompletedWithErrors", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to All files processed successfully!.
/// </summary>
public static string CLI_AllFilesProcessed {
get {
return ResourceManager.GetString("CLI_AllFilesProcessed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No input files or pipe specified. Showing usage..
/// </summary>
public static string CLI_NoInputFiles {
get {
return ResourceManager.GetString("CLI_NoInputFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Warning: Size index {0} is invalid. Using custom size..
/// </summary>
public static string CLI_WarningInvalidSizeIndex {
get {
return ResourceManager.GetString("CLI_WarningInvalidSizeIndex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Current Configuration:.
/// </summary>
public static string CLI_ConfigTitle {
get {
return ResourceManager.GetString("CLI_ConfigTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to General Settings:.
/// </summary>
public static string CLI_ConfigGeneralSettings {
get {
return ResourceManager.GetString("CLI_ConfigGeneralSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shrink only: {0}.
/// </summary>
public static string CLI_ConfigShrinkOnly {
get {
return ResourceManager.GetString("CLI_ConfigShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace original: {0}.
/// </summary>
public static string CLI_ConfigReplaceOriginal {
get {
return ResourceManager.GetString("CLI_ConfigReplaceOriginal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore orientation: {0}.
/// </summary>
public static string CLI_ConfigIgnoreOrientation {
get {
return ResourceManager.GetString("CLI_ConfigIgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove metadata: {0}.
/// </summary>
public static string CLI_ConfigRemoveMetadata {
get {
return ResourceManager.GetString("CLI_ConfigRemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep date modified: {0}.
/// </summary>
public static string CLI_ConfigKeepDateModified {
get {
return ResourceManager.GetString("CLI_ConfigKeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to JPEG quality: {0}.
/// </summary>
public static string CLI_ConfigJpegQuality {
get {
return ResourceManager.GetString("CLI_ConfigJpegQuality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PNG interlace: {0}.
/// </summary>
public static string CLI_ConfigPngInterlace {
get {
return ResourceManager.GetString("CLI_ConfigPngInterlace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TIFF compress: {0}.
/// </summary>
public static string CLI_ConfigTiffCompress {
get {
return ResourceManager.GetString("CLI_ConfigTiffCompress", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Filename format: {0}.
/// </summary>
public static string CLI_ConfigFilenameFormat {
get {
return ResourceManager.GetString("CLI_ConfigFilenameFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom Size:.
/// </summary>
public static string CLI_ConfigCustomSize {
get {
return ResourceManager.GetString("CLI_ConfigCustomSize", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width: {0}.
/// </summary>
public static string CLI_ConfigWidth {
get {
return ResourceManager.GetString("CLI_ConfigWidth", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height: {0}.
/// </summary>
public static string CLI_ConfigHeight {
get {
return ResourceManager.GetString("CLI_ConfigHeight", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fit mode: {0}.
/// </summary>
public static string CLI_ConfigFitMode {
get {
return ResourceManager.GetString("CLI_ConfigFitMode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preset Sizes: (* = currently selected).
/// </summary>
public static string CLI_ConfigPresetSizes {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}: {1} x {2} ({3}).
/// </summary>
public static string CLI_ConfigPresetSizeFormat {
get {
return ResourceManager.GetString("CLI_ConfigPresetSizeFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to → Custom size selected.
/// </summary>
public static string CLI_ConfigCustomSelected {
get {
return ResourceManager.GetString("CLI_ConfigCustomSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image Resizer CLI.
/// </summary>
public static string CLI_UsageTitle {
get {
return ResourceManager.GetString("CLI_UsageTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Usage: PowerToys.ImageResizer.exe [options] &lt;files&gt;.
/// </summary>
public static string CLI_UsageLine {
get {
return ResourceManager.GetString("CLI_UsageLine", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Options:.
/// </summary>
public static string CLI_UsageOptions {
get {
return ResourceManager.GetString("CLI_UsageOptions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Examples:.
/// </summary>
public static string CLI_UsageExamples {
get {
return ResourceManager.GetString("CLI_UsageExamples", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --help.
/// </summary>
public static string CLI_UsageExampleHelp {
get {
return ResourceManager.GetString("CLI_UsageExampleHelp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --width 800 --height 600 image.jpg.
/// </summary>
public static string CLI_UsageExampleDimensions {
get {
return ResourceManager.GetString("CLI_UsageExampleDimensions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 50 --unit percent *.jpg.
/// </summary>
public static string CLI_UsageExamplePercent {
get {
return ResourceManager.GetString("CLI_UsageExamplePercent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys.ImageResizer.exe --size 2 image1.png image2.png.
/// </summary>
public static string CLI_UsageExamplePreset {
get {
return ResourceManager.GetString("CLI_UsageExamplePreset", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Destination directory for resized images.
/// </summary>
public static string CLI_Option_Destination {
get {
return ResourceManager.GetString("CLI_Option_Destination", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output filename format (e.g., %1 (%2)).
/// </summary>
public static string CLI_Option_FileName {
get {
return ResourceManager.GetString("CLI_Option_FileName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image files to resize.
/// </summary>
public static string CLI_Option_Files {
get {
return ResourceManager.GetString("CLI_Option_Files", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to How to fit image: fill, fit, stretch.
/// </summary>
public static string CLI_Option_Fit {
get {
return ResourceManager.GetString("CLI_Option_Fit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height of the resized image in pixels.
/// </summary>
public static string CLI_Option_Height {
get {
return ResourceManager.GetString("CLI_Option_Height", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display this help message.
/// </summary>
public static string CLI_Option_Help {
get {
return ResourceManager.GetString("CLI_Option_Help", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignore image orientation metadata.
/// </summary>
public static string CLI_Option_IgnoreOrientation {
get {
return ResourceManager.GetString("CLI_Option_IgnoreOrientation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preserve the original file modification date.
/// </summary>
public static string CLI_Option_KeepDateModified {
get {
return ResourceManager.GetString("CLI_Option_KeepDateModified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set JPEG quality level (1-100).
/// </summary>
public static string CLI_Option_Quality {
get {
return ResourceManager.GetString("CLI_Option_Quality", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove image metadata during resizing.
/// </summary>
public static string CLI_Option_RemoveMetadata {
get {
return ResourceManager.GetString("CLI_Option_RemoveMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace the original image file.
/// </summary>
public static string CLI_Option_Replace {
get {
return ResourceManager.GetString("CLI_Option_Replace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Display current configuration.
/// </summary>
public static string CLI_Option_ShowConfig {
get {
return ResourceManager.GetString("CLI_Option_ShowConfig", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only shrink images, do not enlarge.
/// </summary>
public static string CLI_Option_ShrinkOnly {
get {
return ResourceManager.GetString("CLI_Option_ShrinkOnly", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use preset size by index (0-based).
/// </summary>
public static string CLI_Option_Size {
get {
return ResourceManager.GetString("CLI_Option_Size", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unit of measurement: pixel, percent, cm, inch.
/// </summary>
public static string CLI_Option_Unit {
get {
return ResourceManager.GetString("CLI_Option_Unit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width of the resized image in pixels.
/// </summary>
public static string CLI_Option_Width {
get {
return ResourceManager.GetString("CLI_Option_Width", resourceCulture);
}
}
}
}

View File

@@ -347,4 +347,156 @@
<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">
<value>Processing {0} file(s)...</value>
</data>
<data name="CLI_ProgressFormat" xml:space="preserve">
<value>Progress: {0}% ({1}/{2})</value>
</data>
<data name="CLI_CompletedWithErrors" xml:space="preserve">
<value>Completed with {0} error(s):</value>
</data>
<data name="CLI_AllFilesProcessed" xml:space="preserve">
<value>All files processed successfully.</value>
</data>
<data name="CLI_WarningInvalidSizeIndex" xml:space="preserve">
<value>Warning: Invalid size index {0}. Using default.</value>
</data>
<data name="CLI_NoInputFiles" xml:space="preserve">
<value>No input files or pipe specified. Showing usage.</value>
</data>
<!-- CLI Config display -->
<data name="CLI_ConfigTitle" xml:space="preserve">
<value>ImageResizer - Current Configuration</value>
</data>
<data name="CLI_ConfigGeneralSettings" xml:space="preserve">
<value>General Settings:</value>
</data>
<data name="CLI_ConfigShrinkOnly" xml:space="preserve">
<value> Shrink Only: {0}</value>
</data>
<data name="CLI_ConfigReplaceOriginal" xml:space="preserve">
<value> Replace Original: {0}</value>
</data>
<data name="CLI_ConfigIgnoreOrientation" xml:space="preserve">
<value> Ignore Orientation: {0}</value>
</data>
<data name="CLI_ConfigRemoveMetadata" xml:space="preserve">
<value> Remove Metadata: {0}</value>
</data>
<data name="CLI_ConfigKeepDateModified" xml:space="preserve">
<value> Keep Date Modified: {0}</value>
</data>
<data name="CLI_ConfigJpegQuality" xml:space="preserve">
<value> JPEG Quality: {0}</value>
</data>
<data name="CLI_ConfigPngInterlace" xml:space="preserve">
<value> PNG Interlace: {0}</value>
</data>
<data name="CLI_ConfigTiffCompress" xml:space="preserve">
<value> TIFF Compress: {0}</value>
</data>
<data name="CLI_ConfigFilenameFormat" xml:space="preserve">
<value> Filename Format: {0}</value>
</data>
<data name="CLI_ConfigCustomSize" xml:space="preserve">
<value>Custom Size:</value>
</data>
<data name="CLI_ConfigWidth" xml:space="preserve">
<value> Width: {0} {1}</value>
</data>
<data name="CLI_ConfigHeight" xml:space="preserve">
<value> Height: {0} {1}</value>
</data>
<data name="CLI_ConfigFitMode" xml:space="preserve">
<value> Fit Mode: {0}</value>
</data>
<data name="CLI_ConfigPresetSizes" xml:space="preserve">
<value>Preset Sizes: (* = currently selected)</value>
</data>
<data name="CLI_ConfigPresetSizeFormat" xml:space="preserve">
<value> [{0}]{1} {2}: {3}x{4} {5} ({6})</value>
</data>
<data name="CLI_ConfigCustomSelected" xml:space="preserve">
<value> [Custom]* {0}x{1} {2} ({3})</value>
</data>
<!-- CLI Usage help -->
<data name="CLI_UsageTitle" xml:space="preserve">
<value>ImageResizer - PowerToys Image Resizer CLI</value>
</data>
<data name="CLI_UsageLine" xml:space="preserve">
<value>Usage: PowerToys.ImageResizerCLI.exe [options] [files...]</value>
</data>
<data name="CLI_UsageOptions" xml:space="preserve">
<value>Options:</value>
</data>
<data name="CLI_UsageExamples" xml:space="preserve">
<value>Examples:</value>
</data>
<data name="CLI_UsageExampleHelp" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --help</value>
</data>
<data name="CLI_UsageExampleDimensions" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --width 800 --height 600 image.jpg</value>
</data>
<data name="CLI_UsageExamplePercent" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe -w 50 -h 50 -u Percent *.jpg</value>
</data>
<data name="CLI_UsageExamplePreset" xml:space="preserve">
<value> PowerToys.ImageResizerCLI.exe --size 0 -d "C:\Output" photo.png</value>
</data>
<!-- CLI Option Descriptions -->
<data name="CLI_Option_Destination" xml:space="preserve">
<value>Set destination directory</value>
</data>
<data name="CLI_Option_FileName" xml:space="preserve">
<value>Set output filename format (%1=original name, %2=size name)</value>
</data>
<data name="CLI_Option_Files" xml:space="preserve">
<value>Image files to resize</value>
</data>
<data name="CLI_Option_Fit" xml:space="preserve">
<value>Set fit mode (Fill, Fit, Stretch)</value>
</data>
<data name="CLI_Option_Height" xml:space="preserve">
<value>Set height</value>
</data>
<data name="CLI_Option_Help" xml:space="preserve">
<value>Show help information</value>
</data>
<data name="CLI_Option_IgnoreOrientation" xml:space="preserve">
<value>Ignore image orientation</value>
</data>
<data name="CLI_Option_KeepDateModified" xml:space="preserve">
<value>Keep original date modified</value>
</data>
<data name="CLI_Option_Quality" xml:space="preserve">
<value>Set JPEG quality level (1-100)</value>
</data>
<data name="CLI_Option_Replace" xml:space="preserve">
<value>Replace original files</value>
</data>
<data name="CLI_Option_ShowConfig" xml:space="preserve">
<value>Show current configuration</value>
</data>
<data name="CLI_Option_ShrinkOnly" xml:space="preserve">
<value>Only shrink images, don't enlarge</value>
</data>
<data name="CLI_Option_RemoveMetadata" xml:space="preserve">
<value>Remove metadata from resized images</value>
</data>
<data name="CLI_Option_Size" xml:space="preserve">
<value>Use preset size by index (0-based)</value>
</data>
<data name="CLI_Option_Unit" xml:space="preserve">
<value>Set unit (Pixel, Percent, Inch, Centimeter)</value>
</data>
<data name="CLI_Option_Width" xml:space="preserve">
<value>Set width</value>
</data>
</root>

View File

@@ -15,6 +15,7 @@ using System.IO.Abstractions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Windows.Media.Imaging;
@@ -42,6 +43,7 @@ namespace ImageResizer.Properties
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);