mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-27 22:47:43 +01:00
Compare commits
4 Commits
shawn/AddT
...
yuleng/win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0973810511 | ||
|
|
a7c8951f6c | ||
|
|
a35c0579f0 | ||
|
|
4ba6fd2723 |
270
CLAUDE.md
Normal file
270
CLAUDE.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Microsoft PowerToys** is a collection of utilities for power users to tune and streamline their Windows experience. The codebase includes 25+ utilities like FancyZones, PowerRename, Image Resizer, Command Palette, Keyboard Manager, and more.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Prerequisites
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
- Run automated setup: `.\tools\build\setup-dev-environment.ps1`
|
||||
|
||||
### Common Build Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
|
||||
| Build current folder | `tools\build\build.cmd` |
|
||||
| Build with options | `.\tools\build\build.ps1 -Platform x64 -Configuration Release` |
|
||||
| Build full solution | Open `PowerToys.slnx` in VS and build |
|
||||
| Build installer (Release only) | `.\tools\build\build-installer.ps1 -Platform x64 -Configuration Release` |
|
||||
|
||||
**Important Build Rules:**
|
||||
- Exit code 0 = success; non-zero = failure
|
||||
- On failure, check `build.<config>.<platform>.errors.log` next to the solution/project
|
||||
- For first build or missing NuGet packages, run `build-essentials.cmd` first
|
||||
- Use one terminal per operation (build → test). Don't switch terminals mid-flow
|
||||
- After making changes, `cd` to the project folder (`.csproj`/`.vcxproj`) before building
|
||||
|
||||
### VS Code Tasks
|
||||
- Use `PT: Build Essentials (quick)` for fast runner + settings build
|
||||
- Use `PT: Build (quick)` to build the current directory
|
||||
|
||||
## Testing
|
||||
|
||||
### Finding Tests
|
||||
- Test projects follow the pattern: `<Product>*UnitTests`, `<Product>*UITests`, or `<Product>*FuzzTests`
|
||||
- Located as sibling folders or 1-2 levels up from product code
|
||||
- Examples: `src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj`
|
||||
|
||||
### Running Tests
|
||||
1. **Build the test project first**, wait for exit code 0
|
||||
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
|
||||
3. **Avoid `dotnet test`** - use VS Test Explorer or vstest.console.exe
|
||||
|
||||
### Test Types
|
||||
- **Unit Tests**: Standard dev environment, no extra setup
|
||||
- **UI Tests**: Require WinAppDriver v1.2.1 and Developer Mode ([download](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1))
|
||||
- **Fuzz Tests**: OneFuzz + .NET 8, required for modules handling file I/O or user input
|
||||
|
||||
### Test Discipline
|
||||
- Add or adjust tests when changing behavior
|
||||
- New modules handling file I/O or user input **must** implement fuzzing tests
|
||||
- State why tests were skipped if applicable (e.g., comment-only change)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Repository Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── runner/ # Main PowerToys.exe, tray icon, module loader, hotkey management
|
||||
├── settings-ui/ # WinUI configuration app (communicates via named pipes)
|
||||
├── modules/ # Individual utilities (each in subfolder)
|
||||
│ ├── AdvancedPaste/
|
||||
│ ├── fancyzones/
|
||||
│ ├── imageresizer/
|
||||
│ ├── keyboardmanager/
|
||||
│ ├── launcher/ # PowerToys Run
|
||||
│ └── ...
|
||||
├── common/ # Shared code: logging, IPC, settings, DPI, telemetry
|
||||
└── dsc/ # Desired State Configuration support
|
||||
|
||||
tools/build/ # Build scripts and automation
|
||||
doc/devdocs/ # Developer documentation
|
||||
installer/ # WiX-based installer projects
|
||||
```
|
||||
|
||||
### Module Types
|
||||
|
||||
1. **Simple Modules** (e.g., Mouse Pointer Crosshairs, Find My Mouse)
|
||||
- Entirely contained in the module interface DLL
|
||||
- No external application
|
||||
|
||||
2. **External Application Launchers** (e.g., Color Picker)
|
||||
- Start a separate application (often WPF/WinUI)
|
||||
- Handle hotkey events
|
||||
- Communicate via named pipes or IPC
|
||||
|
||||
3. **Context Handler Modules** (e.g., PowerRename, Image Resizer)
|
||||
- Shell extensions for File Explorer
|
||||
- Add right-click context menu entries
|
||||
|
||||
4. **Registry-based Modules** (e.g., Power Preview)
|
||||
- Register preview handlers and thumbnail providers
|
||||
- Modify registry during enable/disable
|
||||
|
||||
### Module Interface
|
||||
|
||||
All PowerToys modules implement a standardized interface (`src/modules/interface/`) that defines:
|
||||
- Hotkey structure
|
||||
- Name and key for the utility
|
||||
- Enable/disable functionality
|
||||
- Configuration management
|
||||
- Telemetry settings
|
||||
- GPO configuration
|
||||
|
||||
### Settings System
|
||||
|
||||
- **Runner** (`src/runner/`) loads modules and manages their lifecycle
|
||||
- **Settings UI** (`src/settings-ui/`) is a separate process using WinUI 3
|
||||
- Communication via **named pipes** (IPC) between runner and settings
|
||||
- Settings stored as JSON files in `%LOCALAPPDATA%\Microsoft\PowerToys\`
|
||||
- Schema migrations must maintain backward compatibility
|
||||
|
||||
**Important**: When modifying IPC contracts or JSON schemas:
|
||||
- Update both runner and settings-ui
|
||||
- Maintain backward compatibility
|
||||
- See [doc/devdocs/core/settings/runner-ipc.md](doc/devdocs/core/settings/runner-ipc.md)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Before starting**: Ensure there's an issue to track the work
|
||||
2. **Read the file first**: Always use Read tool before modifying files
|
||||
3. **Follow existing patterns**: Match the style and structure of surrounding code
|
||||
4. **Atomic PRs**: One logical change per PR, no drive-by refactors
|
||||
5. **Build discipline**:
|
||||
- `cd` to project folder after making changes
|
||||
- Build using `tools/build/build.cmd`
|
||||
- Wait for exit code 0 before proceeding
|
||||
6. **Test changes**: Build and run tests for affected modules
|
||||
7. **Update signing**: Add new DLLs/executables to `.pipelines/ESRPSigning_core.json`
|
||||
|
||||
### CLI Tools
|
||||
|
||||
Several modules now have CLI support (FancyZones, Image Resizer, File Locksmith):
|
||||
- Use **System.CommandLine** library for argument parsing
|
||||
- Follow `--kebab-case` for long options, `-x` for short
|
||||
- Exit codes: 0 = success, non-zero = failure
|
||||
- Log to both console and file using `ManagedCommon.Logger`
|
||||
- Reference: [doc/devdocs/cli-conventions.md](doc/devdocs/cli-conventions.md)
|
||||
|
||||
### Localization
|
||||
|
||||
- Localization is handled exclusively by internal Microsoft team
|
||||
- **Do not** submit PRs for localization changes
|
||||
- File issues for localization bugs instead
|
||||
|
||||
## Code Style and Conventions
|
||||
|
||||
### Style Enforcement
|
||||
|
||||
- **C#**: Use `src/.editorconfig` and StyleCop.Analyzers (enforced in build)
|
||||
- **C++**: Use `.clang-format` (press `Ctrl+K Ctrl+D` in VS to format)
|
||||
- **XAML**: Use XamlStyler (`.\.pipelines\applyXamlStyling.ps1 -Main`)
|
||||
|
||||
### Formatting
|
||||
|
||||
- Follow existing patterns in the file you're editing
|
||||
- For new code, follow Modern C++ practices and [C++ Core Guidelines](https://github.com/isocpp/CppCoreGuidelines)
|
||||
- C++ formatting script: `src/codeAnalysis/format_sources.ps1`
|
||||
|
||||
### Logging
|
||||
|
||||
- **C++**: Use spdlog (SPD logs) via `src/common/logger/`
|
||||
- **C#**: Use `ManagedCommon.Logger`
|
||||
- **Critical**: Keep hot paths quiet (no logging in hooks or tight loops)
|
||||
- Detailed guidance: [doc/devdocs/development/logging.md](doc/devdocs/development/logging.md)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- MIT license generally acceptable; other licenses require PM approval
|
||||
- All external packages must be listed in `NOTICE.md`
|
||||
- Update `Directory.Packages.props` for NuGet packages (centralized package management)
|
||||
- Sign new DLLs by adding to signing config
|
||||
|
||||
## Critical Areas Requiring Extra Care
|
||||
|
||||
| Area | Concern | Reference |
|
||||
|------|---------|-----------|
|
||||
| `src/common/` | ABI breaks affect all modules | [.github/instructions/common-libraries.instructions.md](.github/instructions/common-libraries.instructions.md) |
|
||||
| `src/runner/`, `src/settings-ui/` | IPC contracts, schema migrations | [.github/instructions/runner-settings-ui.instructions.md](.github/instructions/runner-settings-ui.instructions.md) |
|
||||
| Installer files | Release impact | Careful review required |
|
||||
| Elevation/GPO logic | Security implications | Confirm no policy handling regression |
|
||||
|
||||
## Key Development Rules
|
||||
|
||||
### Do
|
||||
- Add tests when changing behavior
|
||||
- Follow existing code patterns
|
||||
- Use atomic PRs (one logical change)
|
||||
- Ask for clarification when spec is ambiguous
|
||||
- Check exit codes (`0` = success)
|
||||
- Read files before modifying them
|
||||
- Update `NOTICE.md` when adding dependencies
|
||||
|
||||
### Don't
|
||||
- Don't break IPC/JSON contracts without updating both runner and settings-ui
|
||||
- Don't add noisy logs in hot paths (hooks, tight loops)
|
||||
- Don't introduce third-party dependencies without PM approval
|
||||
- Don't merge incomplete features into main (use feature branches)
|
||||
- Don't use `dotnet test` (use VS Test Explorer or vstest.console.exe)
|
||||
- Don't skip hooks (--no-verify) unless explicitly requested
|
||||
|
||||
## Special Testing Requirements
|
||||
|
||||
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
|
||||
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
|
||||
- **File I/O or user input modules**: Must implement fuzzing tests
|
||||
|
||||
## Running PowerToys
|
||||
|
||||
### Debug Build
|
||||
- After building, run `x64\Debug\PowerToys.exe` directly
|
||||
- Some modules (PowerRename, ImageResizer, File Explorer extensions) require full installation
|
||||
|
||||
### Release Build
|
||||
- Build the installer: `.\tools\build\build-installer.ps1 -Platform x64 -Configuration Release`
|
||||
- Install from `installer\` output folder
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Build Failures
|
||||
1. Check `build.<config>.<platform>.errors.log`
|
||||
2. Ensure submodules are initialized: `git submodule update --init --recursive`
|
||||
3. Run `build-essentials.cmd` to restore NuGet packages
|
||||
4. Check Visual Studio has required workloads (import `.vsconfig`)
|
||||
|
||||
### Missing DLLs at Runtime
|
||||
- Some modules require installation via the installer to register COM handlers/shell extensions
|
||||
- Build and install from `installer/` folder
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Essential Reading
|
||||
- [Architecture Overview](doc/devdocs/core/architecture.md)
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
|
||||
- [Coding Style](doc/devdocs/development/style.md)
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
|
||||
- [Module Interface](doc/devdocs/modules/interface.md)
|
||||
|
||||
### Advanced Topics
|
||||
- [Runner](doc/devdocs/core/runner.md)
|
||||
- [Settings System](doc/devdocs/core/settings/readme.md)
|
||||
- [Logging](doc/devdocs/development/logging.md)
|
||||
- [UI Tests](doc/devdocs/development/ui-tests.md)
|
||||
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
|
||||
- [Installer](doc/devdocs/core/installer.md)
|
||||
|
||||
### Module-Specific Docs
|
||||
- Individual modules: `doc/devdocs/modules/<module-name>.md`
|
||||
- PowerToys Run plugins: `doc/devdocs/modules/launcher/plugins/`
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finishing work:
|
||||
- [ ] Build clean with exit code 0
|
||||
- [ ] Tests updated and passing locally
|
||||
- [ ] No unintended ABI breaks or schema changes
|
||||
- [ ] IPC contracts consistent between runner and settings-ui
|
||||
- [ ] New dependencies added to `NOTICE.md`
|
||||
- [ ] New binaries added to signing config (`.pipelines/ESRPSigning_core.json`)
|
||||
- [ ] PR is atomic (one logical change), with issue linked
|
||||
- [ ] Code follows existing patterns and style guidelines
|
||||
@@ -463,6 +463,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/imageresizer/ui-winui3/ImageResizerUI.WinUI3.csproj">
|
||||
<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" />
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
28
src/modules/imageresizer/ui-winui3/Cli/CliLogger.cs
Normal file
28
src/modules/imageresizer/ui-winui3/Cli/CliLogger.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
122
src/modules/imageresizer/ui-winui3/Cli/CliSettingsApplier.cs
Normal file
122
src/modules/imageresizer/ui-winui3/Cli/CliSettingsApplier.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_Destination)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_FileName)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui-winui3/Cli/Options/FitOption.cs
Normal file
18
src/modules/imageresizer/ui-winui3/Cli/Options/FitOption.cs
Normal 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[0], Properties.Resources.CLI_Option_Fit)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_Height)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui-winui3/Cli/Options/HelpOption.cs
Normal file
18
src/modules/imageresizer/ui-winui3/Cli/Options/HelpOption.cs
Normal 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[0], Properties.Resources.CLI_Option_Help)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_IgnoreOrientation)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_KeepDateModified)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_RemoveMetadata)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_Replace)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_ShowConfig)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_ShrinkOnly)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/imageresizer/ui-winui3/Cli/Options/SizeOption.cs
Normal file
26
src/modules/imageresizer/ui-winui3/Cli/Options/SizeOption.cs
Normal 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[0], 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.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/modules/imageresizer/ui-winui3/Cli/Options/UnitOption.cs
Normal file
18
src/modules/imageresizer/ui-winui3/Cli/Options/UnitOption.cs
Normal 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[0], Properties.Resources.CLI_Option_Unit)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[0], Properties.Resources.CLI_Option_Width)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.ResourceLoader.GetString("Auto");
|
||||
}
|
||||
|
||||
return value?.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
bool boolValue = value is bool b && b;
|
||||
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, string language)
|
||||
=> value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class EnumValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
var type = value?.GetType();
|
||||
if (type == null || !type.IsEnum)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder
|
||||
.Append(type.Name)
|
||||
.Append('_')
|
||||
.Append(Enum.GetName(type, value));
|
||||
|
||||
var toLower = false;
|
||||
if ((string)parameter == "ToLower")
|
||||
{
|
||||
toLower = true;
|
||||
}
|
||||
else if (parameter != null)
|
||||
{
|
||||
builder
|
||||
.Append('_')
|
||||
.Append(parameter);
|
||||
}
|
||||
|
||||
var targetValue = ResourceLoaderInstance.ResourceLoader.GetString(builder.ToString());
|
||||
|
||||
if (toLower && !string.IsNullOrEmpty(targetValue))
|
||||
{
|
||||
var culture = string.IsNullOrEmpty(language) ? CultureInfo.CurrentCulture : new CultureInfo(language);
|
||||
targetValue = targetValue.ToLower(culture);
|
||||
}
|
||||
|
||||
return targetValue;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> value;
|
||||
}
|
||||
}
|
||||
@@ -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 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, string language) =>
|
||||
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, string language) =>
|
||||
value switch
|
||||
{
|
||||
null => 0,
|
||||
double d when double.IsNaN(d) => 0,
|
||||
string str when !double.TryParse(str, out _) => 0,
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Models;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
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, string language)
|
||||
{
|
||||
if (value is not ResizeSize size)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string EnumToString(Enum value, string parameter = null) =>
|
||||
_enumConverter.Convert(value, typeof(string), parameter, language) as string;
|
||||
|
||||
string DoubleToString(double value) =>
|
||||
_autoDoubleConverter.Convert(value, typeof(string), null, language) 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, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Models;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class SizeTypeToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value != null && value.GetType() == typeof(CustomSize) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using ImageResizer.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class TimeRemainingConverter : IValueConverter
|
||||
{
|
||||
private static CompositeFormat _progressTimeRemainingFormat;
|
||||
|
||||
private static CompositeFormat ProgressTimeRemainingFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_progressTimeRemainingFormat == null)
|
||||
{
|
||||
var formatString = ResourceLoaderInstance.ResourceLoader.GetString("Progress_TimeRemaining");
|
||||
_progressTimeRemainingFormat = CompositeFormat.Parse(formatString);
|
||||
}
|
||||
|
||||
return _progressTimeRemainingFormat;
|
||||
}
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan == TimeSpan.MaxValue || timeSpan.TotalSeconds < 1)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var culture = string.IsNullOrEmpty(language) ? CultureInfo.CurrentCulture : new CultureInfo(language);
|
||||
return string.Format(culture, ProgressTimeRemainingFormat, timeSpan);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#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.Collections.Generic
|
||||
{
|
||||
internal static class ICollectionExtensions
|
||||
{
|
||||
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
collection.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace ImageResizer.Helpers
|
||||
{
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
private static ResourceLoader _resourceLoader;
|
||||
|
||||
internal static ResourceLoader ResourceLoader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_resourceLoader == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_resourceLoader = new Microsoft.Windows.ApplicationModel.Resources.ResourceLoader("PowerToys.ImageResizer.pri", "PowerToys.ImageResizer/Resources");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: try with default resource map name
|
||||
try
|
||||
{
|
||||
_resourceLoader = new Microsoft.Windows.ApplicationModel.Resources.ResourceLoader("PowerToys.ImageResizer.pri");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Last resort: use default resource loader
|
||||
_resourceLoader = new Microsoft.Windows.ApplicationModel.Resources.ResourceLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _resourceLoader;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<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" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.ImageResizer</AssemblyTitle>
|
||||
<AssemblyDescription>PowerToys Image Resizer</AssemblyDescription>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>0436;SA1210;SA1516;CA1305;CA1863;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<Folder Include="Properties\" />
|
||||
</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\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
35
src/modules/imageresizer/ui-winui3/ImageResizerXAML/App.xaml
Normal file
35
src/modules/imageresizer/ui-winui3/ImageResizerXAML/App.xaml
Normal file
@@ -0,0 +1,35 @@
|
||||
<Application
|
||||
x:Class="ImageResizer.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:ImageResizer"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default" />
|
||||
<ResourceDictionary x:Key="Light" />
|
||||
<ResourceDictionary x:Key="Dark" />
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- Converters -->
|
||||
<converters:AutoDoubleConverter x:Key="AutoDoubleConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:EnumToIntConverter x:Key="EnumToIntConverter" />
|
||||
<converters:EnumValueConverter x:Key="EnumValueConverter" />
|
||||
<converters:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
|
||||
<converters:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
|
||||
<converters:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
|
||||
<converters:TimeRemainingConverter x:Key="TimeRemainingConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter
|
||||
x:Key="InvertedBoolToVisibilityConverter"
|
||||
TrueValue="Collapsed"
|
||||
FalseValue="Visible" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
220
src/modules/imageresizer/ui-winui3/ImageResizerXAML/App.xaml.cs
Normal file
220
src/modules/imageresizer/ui-winui3/ImageResizerXAML/App.xaml.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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.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 and show main window
|
||||
_window = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- 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:views="using:ImageResizer.Views"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
xmlns:winuiEx="using:WinUIEx"
|
||||
Width="360"
|
||||
Height="506"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
mc:Ignorable="d">
|
||||
<ContentPresenter x:Name="contentPresenter" />
|
||||
</winuiEx:WindowEx>
|
||||
@@ -0,0 +1,184 @@
|
||||
// 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 ImageResizer.Helpers;
|
||||
using ImageResizer.ViewModels;
|
||||
using ImageResizer.Views;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace ImageResizer
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx, IMainView
|
||||
{
|
||||
public MainViewModel ViewModel { get; }
|
||||
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = loader.GetString("ImageResizer");
|
||||
Title = title;
|
||||
|
||||
// Center the window on screen
|
||||
this.CenterOnScreen();
|
||||
|
||||
// Set window icon
|
||||
try
|
||||
{
|
||||
var iconPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "ImageResizer", "ImageResizer.ico");
|
||||
if (System.IO.File.Exists(iconPath))
|
||||
{
|
||||
this.SetIcon(iconPath); // WinUIEx extension method
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Icon loading failed, continue without icon
|
||||
}
|
||||
|
||||
// Add Mica backdrop on Windows 11
|
||||
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
|
||||
{
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||
}
|
||||
|
||||
// Listen to ViewModel property changes
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
// Load the ViewModel after window is ready
|
||||
ViewModel.Load(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, DataContext = inputVM };
|
||||
contentPresenter.Content = inputPage;
|
||||
|
||||
// Adjust window height based on selected size type
|
||||
AdjustWindowHeightForInputPage(inputVM);
|
||||
}
|
||||
else if (page is ProgressViewModel progressVM)
|
||||
{
|
||||
var progressPage = new ProgressPage { ViewModel = progressVM, DataContext = progressVM };
|
||||
contentPresenter.Content = progressPage;
|
||||
|
||||
// Fixed height for progress page
|
||||
this.Height = 400;
|
||||
}
|
||||
else if (page is ResultsViewModel resultsVM)
|
||||
{
|
||||
var resultsPage = new ResultsPage { ViewModel = resultsVM, DataContext = resultsVM };
|
||||
contentPresenter.Content = resultsPage;
|
||||
|
||||
// Fixed height for results page
|
||||
this.Height = 450;
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustWindowHeightForInputPage(InputViewModel inputVM)
|
||||
{
|
||||
// Subscribe to SelectedSize changes to adjust height dynamically
|
||||
inputVM.Settings.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(inputVM.Settings.SelectedSize))
|
||||
{
|
||||
UpdateWindowHeightForSelectedSize(inputVM.Settings.SelectedSize);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial height
|
||||
UpdateWindowHeightForSelectedSize(inputVM.Settings.SelectedSize);
|
||||
}
|
||||
|
||||
private void UpdateWindowHeightForSelectedSize(ImageResizer.Models.ResizeSize selectedSize)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (selectedSize is ImageResizer.Models.CustomSize)
|
||||
{
|
||||
// Custom template with additional controls
|
||||
this.Height = 640;
|
||||
}
|
||||
else if (selectedSize is ImageResizer.Models.AiSize)
|
||||
{
|
||||
// AI template with slider and descriptions
|
||||
this.Height = 650;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal preset template (Small, Medium, Large, Phone)
|
||||
this.Height = 506;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerable<string> OpenPictureFiles()
|
||||
{
|
||||
var picker = new FileOpenPicker();
|
||||
|
||||
// Initialize the picker with the window handle
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
picker.ViewMode = PickerViewMode.Thumbnail;
|
||||
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
|
||||
picker.FileTypeFilter.Add(".bmp");
|
||||
picker.FileTypeFilter.Add(".dib");
|
||||
picker.FileTypeFilter.Add(".exif");
|
||||
picker.FileTypeFilter.Add(".gif");
|
||||
picker.FileTypeFilter.Add(".jfif");
|
||||
picker.FileTypeFilter.Add(".jpe");
|
||||
picker.FileTypeFilter.Add(".jpeg");
|
||||
picker.FileTypeFilter.Add(".jpg");
|
||||
picker.FileTypeFilter.Add(".jxr");
|
||||
picker.FileTypeFilter.Add(".png");
|
||||
picker.FileTypeFilter.Add(".rle");
|
||||
picker.FileTypeFilter.Add(".tif");
|
||||
picker.FileTypeFilter.Add(".tiff");
|
||||
picker.FileTypeFilter.Add(".wdp");
|
||||
|
||||
var files = picker.PickMultipleFilesAsync().AsTask().GetAwaiter().GetResult();
|
||||
if (files != null && files.Count > 0)
|
||||
{
|
||||
return files.Select(f => f.Path);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
void IMainView.Close()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public interface IMainView
|
||||
{
|
||||
IEnumerable<string> OpenPictureFiles();
|
||||
|
||||
void Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<!-- 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:converters="using:ImageResizer.Converters"
|
||||
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:ImageResizer.Views"
|
||||
xmlns:m="using:ImageResizer.Models"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
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">
|
||||
<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 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="{StaticResource 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">
|
||||
<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" 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>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="16">
|
||||
<ComboBox
|
||||
x:Name="SizeComboBox"
|
||||
Height="64"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource SizeTemplateSelector}"
|
||||
ItemsSource="{Binding Settings.AllSizes, Mode=OneWay}"
|
||||
SelectedItem="{Binding Settings.SelectedSize, Mode=TwoWay}" />
|
||||
</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="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Margin="16">
|
||||
<!-- AI Model Download Prompt -->
|
||||
<StackPanel Visibility="{Binding ShowModelDownloadPrompt, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{Binding ModelStatusMessage, Mode=OneWay}"
|
||||
Severity="Informational" />
|
||||
|
||||
<Button
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding DownloadModelCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="{Binding IsModelDownloading, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}, ConverterParameter=Inverted}">
|
||||
<TextBlock x:Uid="Input_AiModelDownloadButton" />
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Margin="0,8,0,0"
|
||||
Visibility="{Binding IsModelDownloading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<!-- ProgressRing commented out for WPF compatibility -->
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding ModelStatusMessage, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI Scale Controls -->
|
||||
<StackPanel Visibility="{Binding ShowAiControls, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="Input_AiCurrentLabel" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding AiScaleDisplay, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<Slider
|
||||
Margin="0,8,0,0"
|
||||
Maximum="8"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="BottomRight"
|
||||
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0,16,0,0"
|
||||
Visibility="{Binding ShowAiSizeDescriptions, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="Input_AiCurrentLabel" Foreground="{StaticResource TextFillColorSecondaryBrush}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding CurrentResolutionDescription, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<TextBlock x:Uid="Input_AiNewLabel" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding NewResolutionDescription, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Custom input matrix -->
|
||||
<Grid
|
||||
Margin="16"
|
||||
Visibility="{Binding Settings.SelectedSize, Mode=OneWay, 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>
|
||||
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<NumberBox
|
||||
x:Name="WidthNumberBox"
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
KeyDown="NumberBox_KeyDown"
|
||||
Minimum="0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding Settings.SelectedSize.Width, Mode=TwoWay}" />
|
||||
|
||||
<FontIcon
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph=""
|
||||
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<NumberBox
|
||||
x:Name="HeightNumberBox"
|
||||
Grid.Column="4"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
KeyDown="NumberBox_KeyDown"
|
||||
Minimum="0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding Settings.SelectedSize.Height, Mode=TwoWay}"
|
||||
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
|
||||
<FontIcon
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding ResizeFitValues, Mode=OneWay}"
|
||||
SelectedIndex="{Binding Settings.SelectedSize.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="2"
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="4"
|
||||
Margin="8,0,0,0"
|
||||
ItemsSource="{Binding ResizeUnitValues, Mode=OneWay}"
|
||||
SelectedIndex="{Binding Settings.SelectedSize.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"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding Settings.ShrinkOnly, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_ShrinkOnly" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.IgnoreOrientation, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_IgnoreOrientation" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.Replace, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_Replace" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.RemoveMetadata, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_RemoveMetadata" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Separator line -->
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Height="1"
|
||||
Margin="0,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Background="{StaticResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<InfoBar
|
||||
Grid.Row="3"
|
||||
Margin="16,0"
|
||||
IsClosable="False"
|
||||
IsOpen="{Binding TryingToResizeGifFiles, Mode=OneWay}"
|
||||
Severity="Warning">
|
||||
<TextBlock x:Uid="Input_GifWarning" />
|
||||
</InfoBar>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid Grid.Row="4" Margin="16,8,16,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Command="{Binding OpenSettingsCommand}">
|
||||
<FontIcon FontSize="20" Glyph="" />
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="Open_settings" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
MinWidth="76"
|
||||
Command="{Binding ResizeCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Input_Resize" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
MinWidth="76"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding CancelCommand}">
|
||||
<TextBlock x:Uid="Cancel" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 NumberBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Enter)
|
||||
{
|
||||
var numberBox = sender as NumberBox;
|
||||
if (numberBox != null && ViewModel != null)
|
||||
{
|
||||
KeyPressParams keyParams;
|
||||
var value = numberBox.Value;
|
||||
|
||||
if (!double.IsNaN(value))
|
||||
{
|
||||
switch (numberBox.Name)
|
||||
{
|
||||
case "WidthNumberBox":
|
||||
keyParams = new KeyPressParams
|
||||
{
|
||||
Value = value,
|
||||
Dimension = Dimension.Width,
|
||||
};
|
||||
break;
|
||||
|
||||
case "HeightNumberBox":
|
||||
keyParams = new KeyPressParams
|
||||
{
|
||||
Value = value,
|
||||
Dimension = Dimension.Height,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.EnterKeyPressedCommand.Execute(keyParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public partial class PageTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate InputTemplate { get; set; }
|
||||
|
||||
public DataTemplate ProgressTemplate { get; set; }
|
||||
|
||||
public DataTemplate ResultsTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
InputViewModel => InputTemplate,
|
||||
ProgressViewModel => ProgressTemplate,
|
||||
ResultsViewModel => ResultsTemplate,
|
||||
_ => base.SelectTemplateCore(item),
|
||||
};
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return SelectTemplateCore(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<!-- 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:converters="using:ImageResizer.Converters"
|
||||
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="Progress_MainInstruction"
|
||||
Margin="12,12,12,0"
|
||||
FontSize="16" />
|
||||
<TextBlock
|
||||
Margin="12,12,12,0"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding TimeRemaining, Mode=OneWay, Converter={StaticResource TimeRemainingConverter}}" />
|
||||
<ProgressBar
|
||||
Height="16"
|
||||
Margin="12,12,12,0"
|
||||
Maximum="1"
|
||||
Value="{Binding Progress, Mode=OneWay}" />
|
||||
<Border
|
||||
Margin="0,12,0,0"
|
||||
Padding="12"
|
||||
Background="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button MinWidth="76" Command="{Binding StopCommand}">
|
||||
<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,50 @@
|
||||
<!-- 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"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="Results_MainInstruction"
|
||||
Margin="12,12,12,0"
|
||||
FontSize="16" />
|
||||
<ScrollViewer
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<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"
|
||||
Background="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button MinWidth="76" Command="{Binding CloseCommand}">
|
||||
<TextBlock x:Uid="Results_Close" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/modules/imageresizer/ui-winui3/Models/AiSize.cs
Normal file
50
src/modules/imageresizer/ui-winui3/Models/AiSize.cs
Normal 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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public partial class AiSize : ResizeSize
|
||||
{
|
||||
private static CompositeFormat _scaleFormat;
|
||||
|
||||
private static CompositeFormat ScaleFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_scaleFormat == null)
|
||||
{
|
||||
_scaleFormat = CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Input_AiScaleFormat"));
|
||||
}
|
||||
|
||||
return _scaleFormat;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("scale")]
|
||||
private int _scale = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted scale display string (e.g., "2x").
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, Scale);
|
||||
|
||||
[JsonConstructor]
|
||||
public AiSize(int scale)
|
||||
{
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public AiSize()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/modules/imageresizer/ui-winui3/Models/CliOptions.cs
Normal file
185
src/modules/imageresizer/ui-winui3/Models/CliOptions.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
#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
|
||||
{
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
public bool ShowConfig { get; set; }
|
||||
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
public double? Width { get; set; }
|
||||
|
||||
public double? Height { get; set; }
|
||||
|
||||
public ResizeUnit? Unit { get; set; }
|
||||
|
||||
public ResizeFit? Fit { get; set; }
|
||||
|
||||
public int? SizeIndex { get; set; }
|
||||
|
||||
public bool? ShrinkOnly { get; set; }
|
||||
|
||||
public bool? Replace { get; set; }
|
||||
|
||||
public bool? IgnoreOrientation { get; set; }
|
||||
|
||||
public bool? RemoveMetadata { get; set; }
|
||||
|
||||
public int? JpegQualityLevel { get; set; }
|
||||
|
||||
public bool? KeepDateModified { get; set; }
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
public bool? ProgressLines { get; set; }
|
||||
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
public string PipeName { get; set; }
|
||||
|
||||
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
|
||||
|
||||
private static bool? ToBoolOrNull(bool value) => value ? true : null;
|
||||
|
||||
public static CliOptions Parse(string[] args)
|
||||
{
|
||||
var options = new CliOptions();
|
||||
var cmd = new ImageResizerRootCommand();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
|
||||
if (files != null)
|
||||
{
|
||||
const string pipeNamePrefix = "\\\\.\\pipe\\";
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.PipeName = file.Substring(pipeNamePrefix.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.Files.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static void PrintConfig(ImageResizer.Properties.Settings settings)
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigTitle"));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigGeneralSettings"));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigShrinkOnly"), settings.ShrinkOnly));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigReplaceOriginal"), settings.Replace));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigIgnoreOrientation"), settings.IgnoreOrientation));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigRemoveMetadata"), settings.RemoveMetadata));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigKeepDateModified"), settings.KeepDateModified));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigJpegQuality"), settings.JpegQualityLevel));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigPngInterlace"), settings.PngInterlaceOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigTiffCompress"), settings.TiffCompressOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigFilenameFormat"), settings.FileName));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigCustomSize"));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigWidth"), settings.CustomSize.Width, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigHeight"), settings.CustomSize.Height, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigFitMode"), settings.CustomSize.Fit));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(loader.GetString("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, loader.GetString("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, loader.GetString("CLI_ConfigCustomSelected"), settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
|
||||
}
|
||||
}
|
||||
|
||||
public static void PrintUsage()
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine(loader.GetString("CLI_UsageTitle"));
|
||||
Console.WriteLine();
|
||||
|
||||
var cmd = new ImageResizerRootCommand();
|
||||
|
||||
Console.WriteLine(loader.GetString("CLI_UsageLine"));
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine(loader.GetString("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(loader.GetString("CLI_UsageExamples"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExampleHelp"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExampleDimensions"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExamplePercent"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExamplePreset"));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/modules/imageresizer/ui-winui3/Models/CustomSize.cs
Normal file
34
src/modules/imageresizer/ui-winui3/Models/CustomSize.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
#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.Text.Json.Serialization;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class CustomSize : ResizeSize
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override string Name
|
||||
{
|
||||
get => ResourceLoaderInstance.ResourceLoader.GetString("Input_Custom");
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public CustomSize(ResizeFit fit, double width, double height, ResizeUnit unit)
|
||||
{
|
||||
Fit = fit;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Unit = unit;
|
||||
}
|
||||
|
||||
public CustomSize()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/modules/imageresizer/ui-winui3/Models/ResizeBatch.cs
Normal file
189
src/modules/imageresizer/ui-winui3/Models/ResizeBatch.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
#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.Concurrent;
|
||||
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;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class ResizeBatch
|
||||
{
|
||||
private readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
public static void SetAiSuperResolutionService(IAISuperResolutionService service)
|
||||
{
|
||||
_aiSuperResolutionService = service;
|
||||
}
|
||||
|
||||
public static void DisposeAiSuperResolutionService()
|
||||
{
|
||||
_aiSuperResolutionService?.Dispose();
|
||||
_aiSuperResolutionService = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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 && (Console.IsInputRedirected || !ReferenceEquals(standardInput, Console.In)))
|
||||
{
|
||||
while ((file = standardInput.ReadLine()) != null)
|
||||
{
|
||||
// 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(".", options.PipeName, PipeDirection.In))
|
||||
{
|
||||
// Connect to the pipe or wait until the pipe is available.
|
||||
pipeClient.Connect();
|
||||
|
||||
using (StreamReader sr = new StreamReader(pipeClient, Encoding.Unicode))
|
||||
{
|
||||
string file;
|
||||
|
||||
// Display the read text to the console
|
||||
while ((file = sr.ReadLine()) != null)
|
||||
{
|
||||
if (IsValidImagePath(file))
|
||||
{
|
||||
batch.Files.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
// 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(
|
||||
Files,
|
||||
new ParallelOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
},
|
||||
(file, state, i) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Execute(file, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(new ResizeError { File = _fileSystem.Path.GetFileName(file), Error = ex.Message });
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref completed);
|
||||
reportProgress(completed, total);
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
protected virtual void Execute(string file, Settings settings)
|
||||
{
|
||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/modules/imageresizer/ui-winui3/Models/ResizeError.cs
Normal file
15
src/modules/imageresizer/ui-winui3/Models/ResizeError.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
#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.Models
|
||||
{
|
||||
public class ResizeError
|
||||
{
|
||||
public string File { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
16
src/modules/imageresizer/ui-winui3/Models/ResizeFit.cs
Normal file
16
src/modules/imageresizer/ui-winui3/Models/ResizeFit.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#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.Models
|
||||
{
|
||||
public enum ResizeFit
|
||||
{
|
||||
Fill,
|
||||
Fit,
|
||||
Stretch,
|
||||
}
|
||||
}
|
||||
444
src/modules/imageresizer/ui-winui3/Models/ResizeOperation.cs
Normal file
444
src/modules/imageresizer/ui-winui3/Models/ResizeOperation.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
#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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using ImageResizer.Extensions;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Utilities;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
internal class ResizeOperation
|
||||
{
|
||||
private readonly IFileSystem _fileSystem = new System.IO.Abstractions.FileSystem();
|
||||
|
||||
private readonly string _file;
|
||||
private readonly string _destinationDirectory;
|
||||
private readonly Settings _settings;
|
||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||
private static CompositeFormat _aiErrorFormat;
|
||||
|
||||
private static CompositeFormat AiErrorFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_aiErrorFormat == null)
|
||||
{
|
||||
_aiErrorFormat = CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiProcessingFailed"));
|
||||
}
|
||||
|
||||
return _aiErrorFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||
private static readonly string[] _avoidFilenames =
|
||||
{
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
};
|
||||
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
||||
{
|
||||
_file = file;
|
||||
_destinationDirectory = destinationDirectory;
|
||||
_settings = settings;
|
||||
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
string path;
|
||||
using (var inputStream = _fileSystem.File.OpenRead(_file))
|
||||
{
|
||||
var decoder = BitmapDecoder.Create(
|
||||
inputStream,
|
||||
BitmapCreateOptions.PreservePixelFormat,
|
||||
BitmapCacheOption.None);
|
||||
|
||||
var containerFormat = decoder.CodecInfo.ContainerFormat;
|
||||
|
||||
var encoder = CreateEncoder(containerFormat);
|
||||
|
||||
if (decoder.Metadata != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
encoder.Metadata = decoder.Metadata;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder.Palette != null)
|
||||
{
|
||||
encoder.Palette = decoder.Palette;
|
||||
}
|
||||
|
||||
foreach (var originalFrame in decoder.Frames)
|
||||
{
|
||||
var transformedBitmap = Transform(originalFrame);
|
||||
|
||||
// if the frame was not modified, we should not replace the metadata
|
||||
if (transformedBitmap == originalFrame)
|
||||
{
|
||||
encoder.Frames.Add(originalFrame);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (_settings.KeepDateModified)
|
||||
{
|
||||
_fileSystem.File.SetLastWriteTimeUtc(path, _fileSystem.File.GetLastWriteTimeUtc(_file));
|
||||
}
|
||||
|
||||
if (_settings.Replace)
|
||||
{
|
||||
var backup = GetBackupPath();
|
||||
_fileSystem.File.Replace(path, _file, backup, ignoreMetadataErrors: true);
|
||||
FileSystem.DeleteFile(backup, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapEncoder CreateEncoder(Guid containerFormat)
|
||||
{
|
||||
var createdEncoder = BitmapEncoder.Create(containerFormat);
|
||||
if (!createdEncoder.CanEncode())
|
||||
{
|
||||
createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
|
||||
}
|
||||
|
||||
ConfigureEncoder(createdEncoder);
|
||||
|
||||
return createdEncoder;
|
||||
|
||||
void ConfigureEncoder(BitmapEncoder encoder)
|
||||
{
|
||||
switch (encoder)
|
||||
{
|
||||
case JpegBitmapEncoder jpegEncoder:
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapSource Transform(BitmapSource source)
|
||||
{
|
||||
if (_settings.SelectedSize is AiSize)
|
||||
{
|
||||
return TransformWithAi(source);
|
||||
}
|
||||
|
||||
int originalWidth = source.PixelWidth;
|
||||
int originalHeight = source.PixelHeight;
|
||||
|
||||
// Convert from the chosen size unit to pixels, if necessary.
|
||||
double width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
|
||||
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.DpiY);
|
||||
|
||||
// Swap target width/height dimensions if orientation correction is required.
|
||||
bool canSwapDimensions = _settings.IgnoreOrientation &&
|
||||
!_settings.SelectedSize.HasAuto &&
|
||||
_settings.SelectedSize.Unit != ResizeUnit.Percent;
|
||||
|
||||
if (canSwapDimensions)
|
||||
{
|
||||
bool isInputLandscape = originalWidth > originalHeight;
|
||||
bool isInputPortrait = originalHeight > originalWidth;
|
||||
bool isTargetLandscape = width > height;
|
||||
bool isTargetPortrait = height > width;
|
||||
|
||||
// Swap dimensions if there is a mismatch between input and target.
|
||||
if ((isInputLandscape && isTargetPortrait) ||
|
||||
(isInputPortrait && isTargetLandscape))
|
||||
{
|
||||
(width, height) = (height, width);
|
||||
}
|
||||
}
|
||||
|
||||
double scaleX = width / originalWidth;
|
||||
double scaleY = height / originalHeight;
|
||||
|
||||
// Normalize scales based on the chosen Fit/Fill mode.
|
||||
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
|
||||
{
|
||||
scaleX = Math.Min(scaleX, scaleY);
|
||||
scaleY = scaleX;
|
||||
}
|
||||
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
|
||||
{
|
||||
scaleX = Math.Max(scaleX, scaleY);
|
||||
scaleY = scaleX;
|
||||
}
|
||||
|
||||
// Handle Shrink Only mode.
|
||||
if (_settings.ShrinkOnly && _settings.SelectedSize.Unit != ResizeUnit.Percent)
|
||||
{
|
||||
if (scaleX > 1 || scaleY > 1)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
|
||||
(originalWidth > width || originalHeight > height);
|
||||
|
||||
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the scaling.
|
||||
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
|
||||
|
||||
// Apply the centered crop for Fill mode, if necessary.
|
||||
if (_settings.SelectedSize.Fit == ResizeFit.Fill
|
||||
&& (scaledBitmap.PixelWidth > width
|
||||
|| scaledBitmap.PixelHeight > height))
|
||||
{
|
||||
int x = (int)(((originalWidth * scaleX) - width) / 2);
|
||||
int y = (int)(((originalHeight * scaleY) - height) / 2);
|
||||
|
||||
return new CroppedBitmap(scaledBitmap, new Int32Rect(x, y, (int)width, (int)height));
|
||||
}
|
||||
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
private BitmapSource TransformWithAi(BitmapSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _aiSuperResolutionService.ApplySuperResolution(
|
||||
source,
|
||||
_settings.AiSize.Scale,
|
||||
_file);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
|
||||
{
|
||||
if (originalMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
|
||||
if (EnsureFrameIsValid(frameWithOriginalMetadata))
|
||||
{
|
||||
return originalMetadata;
|
||||
}
|
||||
|
||||
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
|
||||
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
|
||||
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
|
||||
{
|
||||
return recreatedMetadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metadata = new BitmapMetadata(originalMetadata.Format);
|
||||
var listOfMetadata = originalMetadata.GetListOfMetadata();
|
||||
foreach (var (metadataPath, value) in listOfMetadata)
|
||||
{
|
||||
if (value is BitmapMetadata bitmapMetadata)
|
||||
{
|
||||
var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
|
||||
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)
|
||||
{
|
||||
return BitmapFrame.Create(
|
||||
transformedBitmap,
|
||||
thumbnail: null,
|
||||
metadata,
|
||||
colorContexts: null);
|
||||
}
|
||||
|
||||
private string GetDestinationPath(BitmapEncoder encoder)
|
||||
{
|
||||
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
|
||||
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
||||
|
||||
var supportedExtensions = encoder.CodecInfo.FileExtensions.Split(',');
|
||||
var extension = _fileSystem.Path.GetExtension(_file);
|
||||
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
extension = supportedExtensions.FirstOrDefault();
|
||||
}
|
||||
|
||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||
? aiSize.ScaleDisplay
|
||||
: _settings.SelectedSize.Name;
|
||||
string sizeNameSanitized = sizeName
|
||||
.Replace('\\', '_')
|
||||
.Replace('/', '_');
|
||||
|
||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
||||
var fileName = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
_settings.FileNameFormat,
|
||||
originalFileName,
|
||||
sizeNameSanitized,
|
||||
selectedWidth,
|
||||
selectedHeight,
|
||||
encoder.Frames[0].PixelWidth,
|
||||
encoder.Frames[0].PixelHeight);
|
||||
|
||||
fileName = fileName
|
||||
.Replace(':', '_')
|
||||
.Replace('*', '_')
|
||||
.Replace('?', '_')
|
||||
.Replace('"', '_')
|
||||
.Replace('<', '_')
|
||||
.Replace('>', '_')
|
||||
.Replace('|', '_');
|
||||
|
||||
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
|
||||
{
|
||||
fileName = fileName + "_";
|
||||
}
|
||||
|
||||
var path = _fileSystem.Path.Combine(directory, fileName + extension);
|
||||
var uniquifier = 1;
|
||||
while (_fileSystem.File.Exists(path))
|
||||
{
|
||||
path = _fileSystem.Path.Combine(directory, fileName + " (" + uniquifier++ + ")" + extension);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private string GetBackupPath()
|
||||
{
|
||||
var directory = _fileSystem.Path.GetDirectoryName(_file);
|
||||
var fileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
||||
var extension = _fileSystem.Path.GetExtension(_file);
|
||||
|
||||
var path = _fileSystem.Path.Combine(directory, fileName + ".bak" + extension);
|
||||
var uniquifier = 1;
|
||||
while (_fileSystem.File.Exists(path))
|
||||
{
|
||||
path = _fileSystem.Path.Combine(directory, fileName + " (" + uniquifier++ + ")" + ".bak" + extension);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/modules/imageresizer/ui-winui3/Models/ResizeSize.cs
Normal file
130
src/modules/imageresizer/ui-winui3/Models/ResizeSize.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
#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;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ImageResizer.Helpers;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public partial class ResizeSize : ObservableObject, IHasId
|
||||
{
|
||||
private static readonly Dictionary<string, string> _tokens = new Dictionary<string, string>
|
||||
{
|
||||
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
|
||||
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
|
||||
["$large$"] = ResourceLoaderInstance.ResourceLoader.GetString("Large"),
|
||||
["$phone$"] = ResourceLoaderInstance.ResourceLoader.GetString("Phone"),
|
||||
};
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("Id")]
|
||||
private int _id;
|
||||
|
||||
private string _name;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("fit")]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||
private ResizeFit _fit = ResizeFit.Fit;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("width")]
|
||||
private double _width;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("height")]
|
||||
private double _height;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("unit")]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||
private ResizeUnit _unit = ResizeUnit.Pixel;
|
||||
|
||||
public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Fit = fit;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Unit = unit;
|
||||
}
|
||||
|
||||
public ResizeSize()
|
||||
{
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public virtual string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, ReplaceTokens(value));
|
||||
}
|
||||
|
||||
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||
|
||||
public bool HasAuto
|
||||
=> Width == 0 || Height == 0 || double.IsNaN(Width) || double.IsNaN(Height);
|
||||
|
||||
public double GetPixelWidth(int originalWidth, double dpi)
|
||||
=> ConvertToPixels(Width, Unit, originalWidth, dpi);
|
||||
|
||||
public double GetPixelHeight(int originalHeight, double dpi)
|
||||
=> ConvertToPixels(
|
||||
Fit != ResizeFit.Stretch && Unit == ResizeUnit.Percent
|
||||
? Width
|
||||
: Height,
|
||||
Unit,
|
||||
originalHeight,
|
||||
dpi);
|
||||
|
||||
private static string ReplaceTokens(string text)
|
||||
=> (text != null && _tokens.TryGetValue(text, out var result))
|
||||
? result
|
||||
: text;
|
||||
|
||||
private double ConvertToPixels(double value, ResizeUnit unit, int originalValue, double dpi)
|
||||
{
|
||||
if (value == 0 || double.IsNaN(value))
|
||||
{
|
||||
if (Fit == ResizeFit.Fit)
|
||||
{
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
Debug.Assert(Fit == ResizeFit.Fill || Fit == ResizeFit.Stretch, "Unexpected ResizeFit value: " + Fit);
|
||||
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
switch (unit)
|
||||
{
|
||||
case ResizeUnit.Inch:
|
||||
return value * dpi;
|
||||
|
||||
case ResizeUnit.Centimeter:
|
||||
return value * dpi / 2.54;
|
||||
|
||||
case ResizeUnit.Percent:
|
||||
return value / 100 * originalValue;
|
||||
|
||||
default:
|
||||
Debug.Assert(unit == ResizeUnit.Pixel, "Unexpected unit value: " + unit);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/modules/imageresizer/ui-winui3/Models/ResizeUnit.cs
Normal file
17
src/modules/imageresizer/ui-winui3/Models/ResizeUnit.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
#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.Models
|
||||
{
|
||||
public enum ResizeUnit
|
||||
{
|
||||
Centimeter,
|
||||
Inch,
|
||||
Percent,
|
||||
Pixel,
|
||||
}
|
||||
}
|
||||
27
src/modules/imageresizer/ui-winui3/Program.cs
Normal file
27
src/modules/imageresizer/ui-winui3/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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/modules/imageresizer/ui-winui3/Properties/Resources.cs
Normal file
59
src/modules/imageresizer/ui-winui3/Properties/Resources.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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.
|
||||
/// Wraps ResourceLoader for resource string access.
|
||||
/// </summary>
|
||||
internal static class Resources
|
||||
{
|
||||
public static string CLI_Option_Destination => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination");
|
||||
|
||||
public static string CLI_Option_FileName => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_FileName");
|
||||
|
||||
public static string CLI_Option_Files => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Files");
|
||||
|
||||
public static string CLI_Option_Fit => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Fit");
|
||||
|
||||
public static string CLI_Option_Height => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Height");
|
||||
|
||||
public static string CLI_Option_Help => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Help");
|
||||
|
||||
public static string CLI_Option_IgnoreOrientation => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_IgnoreOrientation");
|
||||
|
||||
public static string CLI_Option_KeepDateModified => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_KeepDateModified");
|
||||
|
||||
public static string CLI_Option_Quality => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Quality");
|
||||
|
||||
public static string CLI_Option_Replace => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Replace");
|
||||
|
||||
public static string CLI_Option_ShowConfig => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_ShowConfig");
|
||||
|
||||
public static string CLI_Option_ShrinkOnly => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_ShrinkOnly");
|
||||
|
||||
public static string CLI_Option_RemoveMetadata => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_RemoveMetadata");
|
||||
|
||||
public static string CLI_Option_Size => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Size");
|
||||
|
||||
public static string CLI_Option_Unit => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Unit");
|
||||
|
||||
public static string CLI_Option_Width => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Width");
|
||||
|
||||
public static string CLI_ProcessingFiles => ResourceLoaderInstance.ResourceLoader.GetString("CLI_ProcessingFiles");
|
||||
|
||||
public static string CLI_ProgressFormat => ResourceLoaderInstance.ResourceLoader.GetString("CLI_ProgressFormat");
|
||||
|
||||
public static string CLI_CompletedWithErrors => ResourceLoaderInstance.ResourceLoader.GetString("CLI_CompletedWithErrors");
|
||||
|
||||
public static string CLI_AllFilesProcessed => ResourceLoaderInstance.ResourceLoader.GetString("CLI_AllFilesProcessed");
|
||||
|
||||
public static string CLI_WarningInvalidSizeIndex => ResourceLoaderInstance.ResourceLoader.GetString("CLI_WarningInvalidSizeIndex");
|
||||
|
||||
public static string CLI_NoInputFiles => ResourceLoaderInstance.ResourceLoader.GetString("CLI_NoInputFiles");
|
||||
}
|
||||
}
|
||||
595
src/modules/imageresizer/ui-winui3/Properties/Settings.cs
Normal file
595
src/modules/imageresizer/ui-winui3/Properties/Settings.cs
Normal file
@@ -0,0 +1,595 @@
|
||||
#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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
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;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the availability state of AI Super Resolution feature.
|
||||
/// </summary>
|
||||
public enum AiAvailabilityState
|
||||
{
|
||||
NotSupported, // System doesn't support AI (architecture issue or policy disabled)
|
||||
ModelNotReady, // AI supported but model not downloaded
|
||||
Ready, // AI fully ready to use
|
||||
}
|
||||
|
||||
public sealed partial class Settings : IDataErrorInfo, INotifyPropertyChanged
|
||||
{
|
||||
private static readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
WriteIndented = true,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||
};
|
||||
|
||||
// Cached UI thread DispatcherQueue for cross-thread property change notifications
|
||||
private static DispatcherQueue _uiDispatcherQueue;
|
||||
|
||||
private static CompositeFormat _valueMustBeBetween;
|
||||
|
||||
private static CompositeFormat ValueMustBeBetween
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_valueMustBeBetween == null)
|
||||
{
|
||||
_valueMustBeBetween = System.Text.CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("ValueMustBeBetween"));
|
||||
}
|
||||
|
||||
return _valueMustBeBetween;
|
||||
}
|
||||
}
|
||||
|
||||
// Used to synchronize access to the settings.json file
|
||||
private static Mutex _jsonMutex = new Mutex();
|
||||
private static string _settingsPath = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "Image Resizer", "settings.json");
|
||||
private string _fileNameFormat;
|
||||
private bool _shrinkOnly;
|
||||
private int _selectedSizeIndex;
|
||||
private bool _replace;
|
||||
private bool _ignoreOrientation;
|
||||
private bool _removeMetadata;
|
||||
private int _jpegQualityLevel;
|
||||
private PngInterlaceOption _pngInterlaceOption;
|
||||
private TiffCompressOption _tiffCompressOption;
|
||||
private string _fileName;
|
||||
private bool _keepDateModified;
|
||||
private System.Guid _fallbackEncoder;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
SelectedSizeIndex = 0;
|
||||
ShrinkOnly = false;
|
||||
Replace = false;
|
||||
IgnoreOrientation = true;
|
||||
RemoveMetadata = false;
|
||||
JpegQualityLevel = 90;
|
||||
PngInterlaceOption = System.Windows.Media.Imaging.PngInterlaceOption.Default;
|
||||
TiffCompressOption = System.Windows.Media.Imaging.TiffCompressOption.Default;
|
||||
FileName = "%1 (%2)";
|
||||
Sizes = new ObservableCollection<ResizeSize>
|
||||
{
|
||||
new ResizeSize(0, "$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
|
||||
new ResizeSize(1, "$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
|
||||
new ResizeSize(2, "$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
|
||||
new ResizeSize(3, "$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
|
||||
};
|
||||
KeepDateModified = false;
|
||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||
AiSize = new AiSize(2);
|
||||
AllSizes = new AllSizesCollection(this);
|
||||
}
|
||||
|
||||
private void ValidateSelectedSizeIndex()
|
||||
{
|
||||
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||
? Sizes.Count
|
||||
: Sizes.Count + 1;
|
||||
|
||||
if (_selectedSizeIndex > maxIndex)
|
||||
{
|
||||
_selectedSizeIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||
|
||||
public string FileNameFormat
|
||||
=> _fileNameFormat
|
||||
?? (_fileNameFormat = FileName
|
||||
.Replace("{", "{{", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("}", "}}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%1", "{0}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%2", "{1}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%3", "{2}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%4", "{3}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%5", "{4}", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%6", "{5}", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
[JsonIgnore]
|
||||
public ResizeSize SelectedSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count)
|
||||
{
|
||||
return Sizes[SelectedSizeIndex];
|
||||
}
|
||||
else if (SelectedSizeIndex == Sizes.Count)
|
||||
{
|
||||
return CustomSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && SelectedSizeIndex == Sizes.Count + 1)
|
||||
{
|
||||
return AiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return CustomSize;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
var index = Sizes.IndexOf(value);
|
||||
if (index == -1)
|
||||
{
|
||||
if (value is AiSize)
|
||||
{
|
||||
index = Sizes.Count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = Sizes.Count;
|
||||
}
|
||||
}
|
||||
|
||||
SelectedSizeIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
string IDataErrorInfo.Error => string.Empty;
|
||||
|
||||
string IDataErrorInfo.this[string columnName]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (columnName != nameof(JpegQualityLevel))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, ValueMustBeBetween, 1, 100);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private class AllSizesCollection : IEnumerable<ResizeSize>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private ObservableCollection<ResizeSize> _sizes;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public AllSizesCollection(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_sizes = settings.Sizes;
|
||||
_customSize = settings.CustomSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
|
||||
settings.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Models.CustomSize))
|
||||
{
|
||||
_customSize = settings.CustomSize;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
_aiSize = settings.AiSize;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
var oldSizes = _sizes;
|
||||
oldSizes.CollectionChanged -= HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)oldSizes).PropertyChanged -= HandlePropertyChanged;
|
||||
_sizes = settings.Sizes;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public int Count
|
||||
=> _sizes.Count + 1 + (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported ? 1 : 0);
|
||||
|
||||
public ResizeSize this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < _sizes.Count)
|
||||
{
|
||||
return _sizes[index];
|
||||
}
|
||||
else if (index == _sizes.Count)
|
||||
{
|
||||
return _customSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && index == _sizes.Count + 1)
|
||||
{
|
||||
return _aiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for AllSizesCollection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<ResizeSize> GetEnumerator()
|
||||
=> new AllSizesEnumerator(this);
|
||||
|
||||
private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
=> OnCollectionChanged(e);
|
||||
|
||||
private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
=> PropertyChanged?.Invoke(this, e);
|
||||
|
||||
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
=> CollectionChanged?.Invoke(this, e);
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
private class AllSizesEnumerator : IEnumerator<ResizeSize>
|
||||
{
|
||||
private readonly AllSizesCollection _list;
|
||||
private int _index = -1;
|
||||
|
||||
public AllSizesEnumerator(AllSizesCollection list)
|
||||
=> _list = list;
|
||||
|
||||
public ResizeSize Current
|
||||
=> _list[_index];
|
||||
|
||||
object IEnumerator.Current
|
||||
=> Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
=> ++_index < _list.Count;
|
||||
|
||||
public void Reset()
|
||||
=> _index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static Settings defaultInstance = new Settings();
|
||||
|
||||
[JsonIgnore]
|
||||
public static Settings Default
|
||||
{
|
||||
get
|
||||
{
|
||||
defaultInstance.Reload();
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_selectedSizeIndex")]
|
||||
public int SelectedSizeIndex
|
||||
{
|
||||
get => _selectedSizeIndex;
|
||||
set
|
||||
{
|
||||
_selectedSizeIndex = value;
|
||||
NotifyPropertyChanged();
|
||||
NotifyPropertyChanged(nameof(SelectedSize));
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_shrinkOnly")]
|
||||
public bool ShrinkOnly
|
||||
{
|
||||
get => _shrinkOnly;
|
||||
set
|
||||
{
|
||||
_shrinkOnly = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_replace")]
|
||||
public bool Replace
|
||||
{
|
||||
get => _replace;
|
||||
set
|
||||
{
|
||||
_replace = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_ignoreOrientation")]
|
||||
public bool IgnoreOrientation
|
||||
{
|
||||
get => _ignoreOrientation;
|
||||
set
|
||||
{
|
||||
_ignoreOrientation = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_removeMetadata")]
|
||||
public bool RemoveMetadata
|
||||
{
|
||||
get => _removeMetadata;
|
||||
set
|
||||
{
|
||||
_removeMetadata = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_jpegQualityLevel")]
|
||||
public int JpegQualityLevel
|
||||
{
|
||||
get => _jpegQualityLevel;
|
||||
set
|
||||
{
|
||||
_jpegQualityLevel = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_pngInterlaceOption")]
|
||||
public PngInterlaceOption PngInterlaceOption
|
||||
{
|
||||
get => _pngInterlaceOption;
|
||||
set
|
||||
{
|
||||
_pngInterlaceOption = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_tiffCompressOption")]
|
||||
public TiffCompressOption TiffCompressOption
|
||||
{
|
||||
get => _tiffCompressOption;
|
||||
set
|
||||
{
|
||||
_tiffCompressOption = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_fileName")]
|
||||
public string FileName
|
||||
{
|
||||
get => _fileName;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(FileName));
|
||||
}
|
||||
|
||||
_fileName = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_sizes")]
|
||||
public ObservableCollection<ResizeSize> Sizes { get; private set; }
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_keepDateModified")]
|
||||
public bool KeepDateModified
|
||||
{
|
||||
get => _keepDateModified;
|
||||
set
|
||||
{
|
||||
_keepDateModified = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_fallbackEncoder")]
|
||||
public Guid FallbackEncoder
|
||||
{
|
||||
get => _fallbackEncoder;
|
||||
set
|
||||
{
|
||||
_fallbackEncoder = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_customSize")]
|
||||
public CustomSize CustomSize
|
||||
{
|
||||
get => _customSize;
|
||||
set
|
||||
{
|
||||
_customSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_aiSize")]
|
||||
public AiSize AiSize
|
||||
{
|
||||
get => _aiSize;
|
||||
set
|
||||
{
|
||||
_aiSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_jsonMutex.WaitOne();
|
||||
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this }, _jsonSerializerOptions);
|
||||
|
||||
IFileInfo file = _fileSystem.FileInfo.New(SettingsPath);
|
||||
file.Directory.Create();
|
||||
|
||||
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
|
||||
_jsonMutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
string oldSettingsDir = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "ImageResizer");
|
||||
string settingsDir = _fileSystem.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Microsoft", "PowerToys", "Image Resizer");
|
||||
|
||||
if (_fileSystem.Directory.Exists(oldSettingsDir) && !_fileSystem.Directory.Exists(settingsDir))
|
||||
{
|
||||
_fileSystem.Directory.Move(oldSettingsDir, settingsDir);
|
||||
}
|
||||
|
||||
_jsonMutex.WaitOne();
|
||||
if (!_fileSystem.File.Exists(SettingsPath))
|
||||
{
|
||||
_jsonMutex.ReleaseMutex();
|
||||
Save();
|
||||
return;
|
||||
}
|
||||
|
||||
string jsonData = _fileSystem.File.ReadAllText(SettingsPath);
|
||||
var jsonSettings = new Settings();
|
||||
try
|
||||
{
|
||||
jsonSettings = JsonSerializer.Deserialize<SettingsWrapper>(jsonData, _jsonSerializerOptions)?.Properties;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
// Use cached UI DispatcherQueue for cross-thread safety
|
||||
// If we're on the UI thread, execute directly; otherwise dispatch to UI thread
|
||||
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
if (currentDispatcher != null)
|
||||
{
|
||||
// Already on UI thread, execute directly
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
else if (_uiDispatcherQueue != null)
|
||||
{
|
||||
// On background thread, dispatch to UI thread
|
||||
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no dispatcher available (should not happen in normal operation)
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
|
||||
_jsonMutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
private void ReloadCore(Settings jsonSettings)
|
||||
{
|
||||
ShrinkOnly = jsonSettings.ShrinkOnly;
|
||||
Replace = jsonSettings.Replace;
|
||||
IgnoreOrientation = jsonSettings.IgnoreOrientation;
|
||||
RemoveMetadata = jsonSettings.RemoveMetadata;
|
||||
JpegQualityLevel = jsonSettings.JpegQualityLevel;
|
||||
PngInterlaceOption = jsonSettings.PngInterlaceOption;
|
||||
TiffCompressOption = jsonSettings.TiffCompressOption;
|
||||
FileName = jsonSettings.FileName;
|
||||
KeepDateModified = jsonSettings.KeepDateModified;
|
||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||
CustomSize = jsonSettings.CustomSize;
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(2);
|
||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||
|
||||
if (jsonSettings.Sizes.Count > 0)
|
||||
{
|
||||
Sizes.Clear();
|
||||
Sizes.AddRange(jsonSettings.Sizes);
|
||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||
}
|
||||
|
||||
ValidateSelectedSizeIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#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.Text.Json.Serialization;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
public class SettingsWrapper
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "Image Resizer";
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; set; } = "1";
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public Settings Properties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
public class WrappedJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.GetString() != "value")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = (T)JsonSerializer.Deserialize(ref reader, typeof(T), options);
|
||||
reader.Read();
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (writer == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("value");
|
||||
|
||||
JsonSerializer.Serialize(writer, value, typeof(T), options);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
public class WrappedJsonValueConverter : JsonConverterFactory
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override JsonConverter CreateConverter(
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
if (typeToConvert == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type keyType = typeToConvert.UnderlyingSystemType;
|
||||
|
||||
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
|
||||
typeof(WrappedJsonConverter<>).MakeGenericType(keyType));
|
||||
|
||||
return converter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using ImageResizer.Properties;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for caching AI availability detection results.
|
||||
/// Persists results to avoid slow API calls on every startup.
|
||||
/// Runner calls ImageResizer --detect-ai to perform detection,
|
||||
/// and ImageResizer reads the cached result on normal startup.
|
||||
/// </summary>
|
||||
public static class AiAvailabilityCacheService
|
||||
{
|
||||
private const string CacheFileName = "ai_capabilities.json";
|
||||
private const int CacheVersion = 1;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private static string CachePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft",
|
||||
"PowerToys",
|
||||
CacheFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Load AI availability state from cache.
|
||||
/// Returns null if cache doesn't exist, is invalid, or read fails.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState? LoadCache()
|
||||
{
|
||||
// Cache disabled - always return null to use default value
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save AI availability state to cache.
|
||||
/// Called by --detect-ai mode after performing detection.
|
||||
/// </summary>
|
||||
public static void SaveCache(AiAvailabilityState state)
|
||||
{
|
||||
// Cache disabled - do not save anything
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate cache against current system environment.
|
||||
/// Cache is invalid if version, architecture, or Windows build changed.
|
||||
/// </summary>
|
||||
private static bool IsCacheValid(AiCapabilityCache cache)
|
||||
{
|
||||
if (cache == null || cache.Version != CacheVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.Architecture != RuntimeInformation.ProcessArchitecture.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.WindowsBuild != Environment.OSVersion.Version.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model for AI capability cache file.
|
||||
/// </summary>
|
||||
internal sealed class AiCapabilityCache
|
||||
{
|
||||
public int Version { get; set; }
|
||||
|
||||
public int State { get; set; }
|
||||
|
||||
public string WindowsBuild { get; set; }
|
||||
|
||||
public string Architecture { get; set; }
|
||||
|
||||
public string Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public interface IAISuperResolutionService : IDisposable
|
||||
{
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class NoOpAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
public static NoOpAiSuperResolutionService Instance { get; } = new NoOpAiSuperResolutionService();
|
||||
|
||||
private NoOpAiSuperResolutionService()
|
||||
{
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No resources to dispose in no-op implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Windows.AI;
|
||||
using Microsoft.Windows.AI.Imaging;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class WinAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
private readonly ImageScaler _imageScaler;
|
||||
private readonly object _usageLock = new object();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WinAiSuperResolutionService"/> class.
|
||||
/// Private constructor. Use CreateAsync() factory method to create instances.
|
||||
/// </summary>
|
||||
private WinAiSuperResolutionService(ImageScaler imageScaler)
|
||||
{
|
||||
_imageScaler = imageScaler ?? throw new ArgumentNullException(nameof(imageScaler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async factory method to create and initialize WinAiSuperResolutionService.
|
||||
/// Returns null if initialization fails.
|
||||
/// </summary>
|
||||
public static async Task<WinAiSuperResolutionService> CreateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageScaler = await ImageScaler.CreateAsync();
|
||||
if (imageScaler == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WinAiSuperResolutionService(imageScaler);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static AIFeatureReadyState GetModelReadyState()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ImageScaler.GetReadyState();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If we can't get the state, treat it as disabled by user
|
||||
// The caller should check if it's Ready or NotReady
|
||||
return AIFeatureReadyState.DisabledByUser;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync(IProgress<double> progress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operation = ImageScaler.EnsureReadyAsync();
|
||||
|
||||
// Register progress handler if provided
|
||||
if (progress != null)
|
||||
{
|
||||
operation.Progress = (asyncInfo, progressValue) =>
|
||||
{
|
||||
// progressValue is a double representing completion percentage (0.0 to 1.0 or 0 to 100)
|
||||
progress.Report(progressValue);
|
||||
};
|
||||
}
|
||||
|
||||
return await operation;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
if (source == null || _disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Note: filePath parameter reserved for future use (e.g., logging, caching)
|
||||
// Currently not used by the ImageScaler API
|
||||
try
|
||||
{
|
||||
// Convert WPF BitmapSource to WinRT SoftwareBitmap
|
||||
var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
|
||||
if (softwareBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Calculate target dimensions
|
||||
var newWidth = softwareBitmap.PixelWidth * scale;
|
||||
var newHeight = softwareBitmap.PixelHeight * scale;
|
||||
|
||||
// Apply super resolution with thread-safe access
|
||||
// _usageLock protects concurrent access from Parallel.ForEach threads
|
||||
SoftwareBitmap scaledBitmap;
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (scaledBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Convert back to WPF BitmapSource
|
||||
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Any error, return original image gracefully
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
private static SoftwareBitmap ConvertBitmapSourceToSoftwareBitmap(BitmapSource bitmapSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the bitmap is in a compatible format
|
||||
var convertedBitmap = new FormatConvertedBitmap();
|
||||
convertedBitmap.BeginInit();
|
||||
convertedBitmap.Source = bitmapSource;
|
||||
convertedBitmap.DestinationFormat = PixelFormats.Bgra32;
|
||||
convertedBitmap.EndInit();
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra32
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
convertedBitmap.CopyPixels(pixels, stride, 0);
|
||||
|
||||
// Create SoftwareBitmap from pixel data
|
||||
var softwareBitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
width,
|
||||
height,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, (IntPtr)dataInBytes, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource ConvertSoftwareBitmapToBitmapSource(SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert to Bgra8 format if needed
|
||||
var convertedBitmap = SoftwareBitmap.Convert(
|
||||
softwareBitmap,
|
||||
BitmapPixelFormat.Bgra8,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra8
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
using (var buffer = convertedBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy((IntPtr)dataInBytes, pixels, 0, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Create WPF BitmapSource from pixel data
|
||||
var wpfBitmap = BitmapSource.Create(
|
||||
width,
|
||||
height,
|
||||
96, // DPI X
|
||||
96, // DPI Y
|
||||
PixelFormats.Bgra32,
|
||||
null,
|
||||
pixels,
|
||||
stride);
|
||||
|
||||
wpfBitmap.Freeze(); // Make it thread-safe
|
||||
return wpfBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMemoryBufferByteAccess
|
||||
{
|
||||
unsafe void GetBuffer(out byte* buffer, out uint capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ImageScaler implements IDisposable
|
||||
(_imageScaler as IDisposable)?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
413
src/modules/imageresizer/ui-winui3/Strings/en-us/Resources.resw
Normal file
413
src/modules/imageresizer/ui-winui3/Strings/en-us/Resources.resw
Normal file
@@ -0,0 +1,413 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
|
||||
<!-- General strings -->
|
||||
<data name="ImageResizer" xml:space="preserve">
|
||||
<value>Image Resizer</value>
|
||||
<comment>Product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Cancel.Text" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="Height" xml:space="preserve">
|
||||
<value>Height</value>
|
||||
</data>
|
||||
<data name="Width" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="Unit" xml:space="preserve">
|
||||
<value>Unit</value>
|
||||
</data>
|
||||
<data name="AllFilesFilter" xml:space="preserve">
|
||||
<value>All Files</value>
|
||||
</data>
|
||||
<data name="PictureFilter" xml:space="preserve">
|
||||
<value>All Picture Files</value>
|
||||
</data>
|
||||
|
||||
<!-- Input page -->
|
||||
<data name="Input_Auto" xml:space="preserve">
|
||||
<value>(auto)</value>
|
||||
</data>
|
||||
<data name="Input_Content" xml:space="preserve">
|
||||
<value>Select a size</value>
|
||||
</data>
|
||||
<data name="Input_Custom" xml:space="preserve">
|
||||
<value>Custom</value>
|
||||
</data>
|
||||
<data name="Input_IgnoreOrientation.Text" xml:space="preserve">
|
||||
<value>Ignore the orientation of pictures</value>
|
||||
</data>
|
||||
<data name="Input_GifWarning.Text" xml:space="preserve">
|
||||
<value>Gif files with animations may not be correctly resized.</value>
|
||||
</data>
|
||||
<data name="Input_Replace.Text" xml:space="preserve">
|
||||
<value>Overwrite files</value>
|
||||
</data>
|
||||
<data name="Input_Resize.Text" xml:space="preserve">
|
||||
<value>Resize</value>
|
||||
</data>
|
||||
<data name="Input_ShrinkOnly.Text" xml:space="preserve">
|
||||
<value>Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
<data name="Input_RemoveMetadata.Text" xml:space="preserve">
|
||||
<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>
|
||||
|
||||
<!-- AI Super Resolution -->
|
||||
<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>
|
||||
|
||||
<!-- Progress page -->
|
||||
<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>
|
||||
|
||||
<!-- Results page -->
|
||||
<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>
|
||||
|
||||
<!-- Size names -->
|
||||
<data name="Small" xml:space="preserve">
|
||||
<value>Small</value>
|
||||
</data>
|
||||
<data name="Medium" xml:space="preserve">
|
||||
<value>Medium</value>
|
||||
</data>
|
||||
<data name="Large" xml:space="preserve">
|
||||
<value>Large</value>
|
||||
</data>
|
||||
<data name="Phone" xml:space="preserve">
|
||||
<value>Phone</value>
|
||||
</data>
|
||||
|
||||
<!-- Resize Fit options -->
|
||||
<data name="ResizeFit_Fill" xml:space="preserve">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fill_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fills</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit" xml:space="preserve">
|
||||
<value>Fit</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fits within</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch" xml:space="preserve">
|
||||
<value>Stretch</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>stretches to</value>
|
||||
</data>
|
||||
|
||||
<!-- Resize Unit options -->
|
||||
<data name="ResizeUnit_Centimeter" xml:space="preserve">
|
||||
<value>Centimeters</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Inch" xml:space="preserve">
|
||||
<value>Inches</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Percent" xml:space="preserve">
|
||||
<value>Percent</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Pixel" xml:space="preserve">
|
||||
<value>Pixels</value>
|
||||
</data>
|
||||
|
||||
<!-- PNG Interlace options -->
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<data name="ValueMustBeBetween" xml:space="preserve">
|
||||
<value>Value must be between '{0}' and '{1}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
16
src/modules/imageresizer/ui-winui3/Utilities/MathHelpers.cs
Normal file
16
src/modules/imageresizer/ui-winui3/Utilities/MathHelpers.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
358
src/modules/imageresizer/ui-winui3/ViewModels/InputViewModel.cs
Normal file
358
src/modules/imageresizer/ui-winui3/ViewModels/InputViewModel.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Common.UI;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public partial class InputViewModel : ObservableObject
|
||||
{
|
||||
public const int DefaultAiScale = 2;
|
||||
private const int MinAiScale = 1;
|
||||
private const int MaxAiScale = 8;
|
||||
|
||||
public enum Dimension
|
||||
{
|
||||
Width,
|
||||
Height,
|
||||
}
|
||||
|
||||
public class KeyPressParams
|
||||
{
|
||||
public double Value { get; set; }
|
||||
|
||||
public Dimension Dimension { get; set; }
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _currentResolutionDescription;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _newResolutionDescription;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDownloadingModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _modelStatusMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _modelDownloadProgress;
|
||||
|
||||
public InputViewModel(
|
||||
Settings settings,
|
||||
MainViewModel mainViewModel,
|
||||
IMainView mainView,
|
||||
ResizeBatch batch)
|
||||
{
|
||||
_batch = batch;
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainView = mainView;
|
||||
_hasMultipleFiles = _batch?.Files.Count > 1;
|
||||
|
||||
Settings = settings;
|
||||
if (settings != null)
|
||||
{
|
||||
settings.CustomSize.PropertyChanged += (sender, e) => settings.SelectedSize = (CustomSize)sender;
|
||||
settings.AiSize.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(AiSize.Scale))
|
||||
{
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
||||
}
|
||||
|
||||
InitializeAiState();
|
||||
}
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues<ResizeFit>();
|
||||
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues<ResizeUnit>();
|
||||
|
||||
public int AiSuperResolutionScale
|
||||
{
|
||||
get => Settings?.AiSize?.Scale ?? DefaultAiScale;
|
||||
set
|
||||
{
|
||||
if (Settings?.AiSize != null && Settings.AiSize.Scale != value)
|
||||
{
|
||||
Settings.AiSize.Scale = value;
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
||||
|
||||
public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
|
||||
|
||||
public bool ShowModelDownloadPrompt =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
(App.AiAvailabilityState == AiAvailabilityState.ModelNotReady || IsDownloadingModel);
|
||||
|
||||
public bool ShowAiControls =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
App.AiAvailabilityState == AiAvailabilityState.Ready;
|
||||
|
||||
public bool CanResize
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Settings?.SelectedSize is AiSize)
|
||||
{
|
||||
return App.AiAvailabilityState == AiAvailabilityState.Ready;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryingToResizeGifFiles =>
|
||||
_batch?.Files.Any(filename => filename.EndsWith(".gif", StringComparison.InvariantCultureIgnoreCase)) == true;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanResize))]
|
||||
public void Resize()
|
||||
{
|
||||
Settings.Save();
|
||||
_mainViewModel.CurrentPage = new ProgressViewModel(_batch, _mainViewModel, _mainView);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void EnterKeyPressed(KeyPressParams parameters)
|
||||
{
|
||||
switch (parameters.Dimension)
|
||||
{
|
||||
case Dimension.Width:
|
||||
Settings.CustomSize.Width = parameters.Value;
|
||||
break;
|
||||
case Dimension.Height:
|
||||
Settings.CustomSize.Height = parameters.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Cancel()
|
||||
=> _mainView.Close();
|
||||
|
||||
[RelayCommand]
|
||||
public static void OpenSettings()
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task DownloadModelAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsDownloadingModel = true;
|
||||
ModelStatusMessage = ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelDownloading");
|
||||
ModelDownloadProgress = 0;
|
||||
NotifyAiStateChanged();
|
||||
|
||||
var progress = new Progress<double>(value =>
|
||||
{
|
||||
ModelDownloadProgress = value > 1 ? value : value * 100;
|
||||
});
|
||||
|
||||
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
|
||||
|
||||
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||
{
|
||||
ModelDownloadProgress = 100;
|
||||
App.AiAvailabilityState = AiAvailabilityState.Ready;
|
||||
UpdateStatusMessage();
|
||||
|
||||
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelStatusMessage = ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelDownloadFailed");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
ModelStatusMessage = ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelDownloadFailed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsDownloadingModel = false;
|
||||
if (App.AiAvailabilityState != AiAvailabilityState.Ready)
|
||||
{
|
||||
ModelDownloadProgress = 0;
|
||||
}
|
||||
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Settings.SelectedSizeIndex):
|
||||
case nameof(Settings.SelectedSize):
|
||||
NotifyAiStateChanged();
|
||||
UpdateAiDetails();
|
||||
ResizeCommand.NotifyCanExecuteChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAiScaleWithinRange()
|
||||
{
|
||||
if (Settings?.AiSize != null)
|
||||
{
|
||||
Settings.AiSize.Scale = Math.Clamp(Settings.AiSize.Scale, MinAiScale, MaxAiScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAiDetails()
|
||||
{
|
||||
if (Settings == null || Settings.SelectedSize is not AiSize)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureAiScaleWithinRange();
|
||||
|
||||
if (_hasMultipleFiles)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOriginalDimensionsLoaded();
|
||||
|
||||
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
|
||||
CurrentResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||
: ResourceLoaderInstance.ResourceLoader.GetString("Input_AiUnknownSize");
|
||||
|
||||
var scale = Settings.AiSize.Scale;
|
||||
NewResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||
: ResourceLoaderInstance.ResourceLoader.GetString("Input_AiUnknownSize");
|
||||
}
|
||||
|
||||
private static string FormatDimensions(long width, long height)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} x {1}", width, height);
|
||||
}
|
||||
|
||||
private void EnsureOriginalDimensionsLoaded()
|
||||
{
|
||||
if (_originalDimensionsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = _batch?.Files.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(file);
|
||||
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
|
||||
var frame = decoder.Frames.FirstOrDefault();
|
||||
if (frame != null)
|
||||
{
|
||||
_originalWidth = frame.PixelWidth;
|
||||
_originalHeight = frame.PixelHeight;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_originalWidth = null;
|
||||
_originalHeight = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAiState()
|
||||
{
|
||||
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
private void OnAiInitializationCompleted(object sender, AiAvailabilityState finalState)
|
||||
{
|
||||
UpdateStatusMessage();
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
|
||||
private void UpdateStatusMessage()
|
||||
{
|
||||
ModelStatusMessage = App.AiAvailabilityState switch
|
||||
{
|
||||
AiAvailabilityState.Ready => string.Empty,
|
||||
AiAvailabilityState.ModelNotReady => ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelNotAvailable"),
|
||||
AiAvailabilityState.NotSupported => ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelNotSupported"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private void NotifyAiStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsDownloadingModel));
|
||||
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
||||
OnPropertyChanged(nameof(ShowAiControls));
|
||||
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
||||
OnPropertyChanged(nameof(CanResize));
|
||||
}
|
||||
|
||||
private void NotifyAiScaleChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
||||
OnPropertyChanged(nameof(AiScaleDisplay));
|
||||
UpdateAiDetails();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private readonly ResizeBatch _batch;
|
||||
|
||||
[ObservableProperty]
|
||||
private object _currentPage;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _progress;
|
||||
|
||||
public MainViewModel(ResizeBatch batch, Settings settings)
|
||||
{
|
||||
_batch = batch;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Load(IMainView view)
|
||||
{
|
||||
if (_batch.Files.Count == 0)
|
||||
{
|
||||
foreach (var file in view.OpenPictureFiles())
|
||||
{
|
||||
_batch.Files.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
CurrentPage = new InputViewModel(_settings, this, view, _batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Views;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public partial class ProgressViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly ResizeBatch _batch;
|
||||
private readonly IMainView _mainView;
|
||||
private readonly Stopwatch _stopwatch = new Stopwatch();
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _progress;
|
||||
|
||||
[ObservableProperty]
|
||||
private TimeSpan _timeRemaining;
|
||||
|
||||
public ProgressViewModel(
|
||||
ResizeBatch batch,
|
||||
MainViewModel mainViewModel,
|
||||
IMainView mainView)
|
||||
{
|
||||
_batch = batch;
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainView = mainView;
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
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;
|
||||
var timeRemaining = _stopwatch.Elapsed.Multiply((total - completed) / completed);
|
||||
|
||||
// Dispatch UI updates to the UI thread
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
Progress = progress;
|
||||
_mainViewModel.Progress = progress;
|
||||
TimeRemaining = timeRemaining;
|
||||
});
|
||||
},
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
// Dispatch final UI updates to the UI thread
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (errors.Any())
|
||||
{
|
||||
_mainViewModel.Progress = 0;
|
||||
_mainViewModel.CurrentPage = new ResultsViewModel(_mainView, errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainView.Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Stop()
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_mainView.Close();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public partial class ResultsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IMainView _mainView;
|
||||
|
||||
public ResultsViewModel(IMainView mainView, IEnumerable<ResizeError> errors)
|
||||
{
|
||||
_mainView = mainView;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
public IEnumerable<ResizeError> Errors { get; }
|
||||
|
||||
[RelayCommand]
|
||||
public void Close() => _mainView.Close();
|
||||
}
|
||||
}
|
||||
21
src/modules/imageresizer/ui-winui3/app.manifest
Normal file
21
src/modules/imageresizer/ui-winui3/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