Compare commits

...

15 Commits

Author SHA1 Message Date
Yu Leng (from Dev Box)
7580797b9e Fix ImageResizer build artifacts and installer issues
Migrated ESRP signing config to reference only WinUI3Apps paths for ImageResizer files, avoiding root-level artifacts. Added a workaround in generateAllFileComponents.ps1 to strip incomplete ImageResizer build outputs caused by Exe→WinExe ProjectReference. Updated migration notes to document removal of satellite assembly references and explain the root cause and temporary fix for phantom build artifacts. Recommend refactoring to use a shared Library project for CLI dependencies.
2026-02-07 21:05:34 +08:00
Yu Leng (from Dev Box)
d1f918d57d Remove WinUI3AppsInstallFolder resource handling from WiX
Eliminated all references to WinUI3AppsInstallFolder in Resources.wxs, including directory setup, ImageResizer resource component, and uninstall cleanup. The installer no longer manages localized resources for ImageResizer in this folder.
2026-02-07 03:09:07 +08:00
Yu Leng (from Dev Box)
90386d2fb6 Update build event and add WPF-to-WinRT migration notes
Switched MSIX packaging from PreBuild to PostBuild event for improved reliability and quoting. Added MIGRATION-NOTES.md detailing migration from WPF to WinRT imaging APIs, including PNG interlace and metadata stripping limitations, and pixel-level differences.
2026-02-06 21:52:32 +08:00
Yu Leng (from Dev Box)
3cb71b86f3 Refactor: migrate all imaging to WinRT APIs, remove WPF
Major refactor to replace all WPF imaging (System.Windows.Media.Imaging) with Windows.Graphics.Imaging (WinRT) APIs. All image processing, encoding, and decoding now use WinRT types (BitmapDecoder, BitmapEncoder, SoftwareBitmap, etc.), enabling cross-platform support and future-proofing. Updated all batch, progress, and test logic to async/await patterns. Added CodecHelper for encoder/extension mapping and ImagingEnums for encoder options. Removed all WPF-specific code and dependencies. Updated project settings and XAML for WinRT compatibility. The codebase is now ready for WinUI or cross-platform migration.
2026-02-06 18:51:04 +08:00
Yu Leng (from Dev Box)
f319fbfc07 Reformat copyright and license comments in ResultsPage.xaml
Reformatted the copyright and license comments at the top of ResultsPage.xaml for improved readability and consistency. Added a Unicode BOM and extra spaces inside the comment tags. No functional changes to the code.
2026-02-03 10:41:28 +08:00
Yu Leng (from Dev Box)
65db8df0af Refactor XAML formatting and update expect.txt entries
Standardize XAML comments, property order, and namespace formatting for consistency across MainWindow, InputPage, ProgressPage, and App.xaml. Reorder parameters in InvertedBoolToVisibilityConverter. Update "mousepointer" and "THISCOMPONENT" entries in expect.txt. No functional changes.
2026-02-02 19:53:35 +08:00
Yu Leng (from Dev Box)
3fc6df7bd1 Simplify ResourceLoader initialization
Removed resource path argument from ResourceLoader constructor in ResourceLoaderInstance; now only the resource file is specified.
2026-02-02 12:26:42 +08:00
Yu Leng (from Dev Box)
2d0396a9c9 Update resource script paths and disable MSIX prebuild
- Use relative path for resource generation PowerShell scripts in vcxproj files to fix build issues.
- Comment out MSIX package PreBuildEvent in ImageResizerContextMenu.vcxproj.
- Change icon path to use double backslashes in ImageResizerExt.base.rc for better compatibility.
2026-02-02 12:14:28 +08:00
Yu Leng (from Dev Box)
c5f7c54d07 Merge remote-tracking branch 'origin/main' into yuleng/winui3 2026-02-02 11:59:29 +08:00
Yu Leng (from Dev Box)
6580de3be1 Migrate ImageResizer from WPF to WinUI 3
Major refactor: removed all WPF-specific UI, XAML, and helpers; migrated MVVM logic to CommunityToolkit.Mvvm; replaced resource access with WinUI 3 ResourceLoader; updated CLI options and models for compatibility; cleaned up obsolete files and modernized codebase for WinUI 3 and Windows App SDK. Prepares for future AI features and improved testability.
2026-02-02 11:58:58 +08:00
Kai Tao
67d96b0a13 PowerToys extension: Bundle localization files into installer (#45194)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45171
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
<img width="925" height="612" alt="image"
src="https://github.com/user-attachments/assets/214ead95-504a-4e48-bc25-138323d973f9"
/>
2026-02-02 11:31:21 +08:00
Yu Leng (from Dev Box)
83215647b7 Refactor window sizing to auto-fit content dynamically
Replaced fixed window heights with dynamic sizing based on content's desired height, including chrome and padding. Improved event handler management to prevent memory leaks. Increased window width to 400px. Updated InputPage layout for clarity and made ComboBox stretch horizontally.
2026-01-28 15:10:29 +08:00
Yu Leng (from Dev Box)
0973810511 Refine Image Resizer UI; add CLAUDE.md contributor guide
- Add CLAUDE.md with detailed contributor and build/test guidance
- MainWindow: set custom icon, enable Mica, disable minimize
- Dynamically adjust window height based on selected size type
- Refactor InputPage ComboBox to use DataTemplateSelector for size options
- Add DataTemplates for normal, custom, and AI sizes
- Replace warning Borders with InfoBar controls for better UX
- Update "Resize" button icon for improved clarity
- Add SizeDataTemplateSelector.cs for ComboBox templating
- Improves UI flexibility, polish, and maintainability
2026-01-27 15:50:11 +08:00
Yu Leng (from Dev Box)
a7c8951f6c add slnx 2026-01-27 14:29:14 +08:00
Yu Leng (from Dev Box)
a35c0579f0 init winui3 2026-01-27 14:29:07 +08:00
122 changed files with 3097 additions and 4552 deletions

View File

@@ -1074,6 +1074,7 @@ MOVESIZEEND
MOVESIZESTART
MRM
Mrt
mrt
mru
MSAL
msc
@@ -1844,6 +1845,7 @@ TILEDWINDOW
TILLSON
timedate
timediff
timespan
timeutil
TITLEBARINFO
Titlecase

View File

@@ -134,13 +134,13 @@
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
"WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizer.exe",
"WinUI3Apps\\PowerToys.ImageResizer.dll",
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
"PowerToys.ImageResizerExt.dll",
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll",
"WinUI3Apps\\ImageResizerContextMenuPackage.msix",
"PowerToys.LightSwitchModuleInterface.dll",
"LightSwitchService\\PowerToys.LightSwitchService.exe",

270
CLAUDE.md Normal file
View 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

View File

@@ -476,12 +476,15 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<!-- ImageResizer tests temporarily disabled - needs migration from WPF to WinUI3 APIs -->
<!--
<Folder Name="/modules/imageresizer/Tests/">
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
-->
<Folder Name="/modules/interface/">
<File Path="src/modules/interface/powertoy_module_interface.h" />
</Folder>

View File

@@ -9,7 +9,7 @@
<Fragment>
<!-- Resource directories should be added only if the installer is built on the build farm -->
<?ifdef env.IsPipeline?>
<?foreach ParentDirectory in INSTALLFOLDER;WinUI3AppsInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<?foreach ParentDirectory in INSTALLFOLDER;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<DirectoryRef Id="$(var.ParentDirectory)">
<!-- Resource file directories -->
<?foreach Language in $(var.LocLanguageList)?>
@@ -171,12 +171,6 @@
</RegistryKey>
<File Id="FancyZonesEditor_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.FancyZonesEditor.resources.dll" />
</Component>
<Component Id="ImageResizer_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Guid="$(var.CompGUIDPrefix)02">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="ImageResizer_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="ImageResizer_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\WinUI3Apps\$(var.Language)\PowerToys.ImageResizer.resources.dll" />
</Component>
<Component Id="ColorPicker_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)03">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="ColorPicker_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
@@ -367,6 +361,12 @@
</RegistryKey>
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
</Component>
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>
@@ -453,7 +453,6 @@
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall"/>
<?undef IdSafeLanguage?>
<?endforeach?>
</Component>

View File

@@ -131,7 +131,25 @@ if ($platform -ceq "arm64") {
}
#BaseApplications
# WORKAROUND: Exclude ImageResizer files that leak into the root output directory.
# ImageResizerCLI (Exe, SelfContained) has a ProjectReference to ImageResizerUI (WinExe, SelfContained).
# MSBuild copies the referenced WinExe's apphost (.exe, .deps.json, .runtimeconfig.json) to the root
# output directory as a side effect. These files are incomplete (missing the managed .dll) and should
# not be included in the installer. The complete ImageResizer files are in WinUI3Apps/ and are handled
# by WinUI3ApplicationsFiles. TODO: Refactor ImageResizer to use a shared Library project instead.
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
# Remove leaked ImageResizer artifacts from BaseApplications
$baseAppWxsPath = "$PSScriptRoot\BaseApplications.wxs"
$baseAppWxs = Get-Content $baseAppWxsPath -Raw
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.exe;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.deps\.json;?', ''
$baseAppWxs = $baseAppWxs -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json;?', ''
# Clean up trailing/double semicolons left after removal
$baseAppWxs = $baseAppWxs -replace ';;+', ';'
$baseAppWxs = $baseAppWxs -replace '=;', '='
$baseAppWxs = $baseAppWxs -replace ';"', '"'
Set-Content -Path $baseAppWxsPath -Value $baseAppWxs
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
#WinUI3Applications

View File

@@ -20,9 +20,4 @@
<ItemGroup>
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerContextMenu.base.rc ImageResizerContextMenu.rc" />
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerContextMenu.base.rc ImageResizerContextMenu.rc" />
</Target>
<PropertyGroup Label="Globals">
<Keyword>Win32Proj</Keyword>
@@ -50,10 +50,10 @@
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
</Link>
<PreBuildEvent>
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
</PreBuildEvent>
<PostBuildEvent>
<Command>if exist "$(OutDir)ImageResizerContextMenuPackage.msix" del "$(OutDir)ImageResizerContextMenuPackage.msix" /q
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)ImageResizerContextMenuPackage.msix" /nv</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
@@ -73,10 +73,10 @@ MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Comm
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
</Link>
<PreBuildEvent>
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
</PreBuildEvent>
<PostBuildEvent>
<Command>if exist "$(OutDir)ImageResizerContextMenuPackage.msix" del "$(OutDir)ImageResizerContextMenuPackage.msix" /q
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)ImageResizerContextMenuPackage.msix" /nv</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="framework.h" />

View File

@@ -0,0 +1,53 @@
# ImageResizer: WPF to WinRT Imaging Migration Notes
## Overview
The ImageResizer module's core image processing was migrated from WPF (`System.Windows.Media.Imaging` / `PresentationCore.dll`) to WinRT (`Windows.Graphics.Imaging`). The UI had already been migrated to WinUI 3, but the imaging pipeline still relied on WPF APIs, causing runtime failures when deployed via the installer (missing `PresentationCore.dll` in self-contained WinUI 3 deployment).
## Known Limitations
### PNG Interlace Mode Not Configurable
**Setting**: `PngInterlaceOption` (values: `Default`, `On`, `Off`)
**Behavior change**: The WinRT `BitmapEncoder` does not expose an API to configure PNG interlace mode. The `PngInterlaceOption` setting is preserved in `settings.json` for backward compatibility, but it has **no effect** on the encoded PNG output. WinRT's PNG encoder always uses its default interlace behavior (non-interlaced).
**Why**: WPF's `PngBitmapEncoder` had an `Interlace` property that mapped to the Adam7 interlacing algorithm, which enables progressive image loading. WinRT's `BitmapEncoder` does not expose an equivalent property, and there is no `BitmapPropertySet` key to control PNG interlacing.
**Impact**: Minimal. PNG interlacing is primarily useful for slow network transfers (progressive rendering in browsers). For local file resizing, it has negligible impact. The Settings UI still displays the option for JSON compatibility, but changing it will not alter the output.
### Metadata Stripping Behavior Change
**Setting**: `RemoveMetadata = true`
**Old behavior (WPF)**: Metadata was selectively stripped — most EXIF properties were removed, but `System.Photo.Orientation` was preserved to maintain correct image display orientation.
**New behavior (WinRT)**: All metadata is stripped when `RemoveMetadata = true`. The new implementation uses `BitmapEncoder.CreateAsync()` (fresh encoder with no metadata) instead of cloning and selectively clearing metadata. This means orientation EXIF data is also removed.
**Why**: WPF had `BitmapMetadata.Clone()` which allowed selective property removal. WinRT's `BitmapProperties` API does not provide an equivalent way to bulk-remove metadata while preserving specific fields after using `CreateForTranscodingAsync`. The simplest correct approach is to use a fresh encoder, which strips everything.
**Impact**: Images with EXIF orientation tags that are resized with `RemoveMetadata = true` may display rotated in viewers that rely on EXIF orientation rather than actual pixel orientation. This matches the user's explicit intent to remove metadata.
### Pixel-Level Differences
WinRT's `BitmapInterpolationMode.Fant` may produce slightly different pixel values compared to WPF's internal scaling algorithm. Both are high-quality interpolation methods, but they are not guaranteed to produce bit-identical output. This is expected and does not affect visual quality.
## Installer / Build Pipeline Issues
### Satellite Assembly References Removed from Resources.wxs
**Problem**: After migrating to WinUI 3, the WiX installer failed with `WIX0103` errors — it could not find `PowerToys.ImageResizer.resources.dll` satellite assemblies in `WinUI3Apps/{locale}/` directories.
**Root cause**: WPF uses `.resx` files which compile into satellite assemblies (`.resources.dll`). WinUI 3 uses `.resw` files which compile into `.pri` files. The installer's `Resources.wxs` still referenced the old satellite assembly pattern.
**Fix**: Removed the ImageResizer satellite assembly component, `WinUI3AppsInstallFolder` from the `ParentDirectory` foreach loop, and the corresponding `RemoveFolder` entry from `Resources.wxs`.
### Phantom Root-Level Build Artifacts
**Problem**: `PowerToys.ImageResizer.exe`, `.deps.json`, and `.runtimeconfig.json` appear in the root output directory (`x64/Release/`) even though the project's `OutputPath` is `WinUI3Apps/`. This caused the installer to include an incomplete, non-functional copy (missing the managed `.dll`) and the ESRP signing check to fail.
**Root cause**: `ImageResizerCLI` (`OutputType=Exe`, `SelfContained=true`) has a `<ProjectReference>` to `ImageResizerUI` (`OutputType=WinExe`, `SelfContained=true`). When MSBuild processes an Exe→WinExe dependency between two self-contained projects, it copies the referenced project's apphost (`.exe`) and runtime config files to the root output directory as a side effect. This is the **only** Exe→WinExe `ProjectReference` in the entire PowerToys codebase — other CLI tools (e.g., FancyZonesCLI) correctly reference a shared Library project instead.
**Temporary fix**: `generateAllFileComponents.ps1` strips the leaked ImageResizer files from the `BaseApplicationsFiles` list after generation. The signing config (`ESRPSigning_core.json`) only references `WinUI3Apps\\` paths.
**TODO**: Refactor the dependency — extract shared CLI logic from `ImageResizerUI` (`ui/Cli/` folder) into a separate Library project, so `ImageResizerCLI` references a Library instead of a WinExe. This follows the pattern used by `FancyZonesCLI``FancyZonesEditorCommon`.

View File

@@ -82,4 +82,4 @@ IDR_CONTEXTMENUHANDLER REGISTRY "ContextMenuHandler.rgs"
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_RESIZE_PICTURES ICON "..\ui\Resources\ImageResizer.ico"
IDI_RESIZE_PICTURES ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"

View File

@@ -2,7 +2,7 @@
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerExt.base.rc ImageResizerExt.rc" />
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerExt.base.rc ImageResizerExt.rc" />
</Target>
<PropertyGroup Label="Globals">
<ProjectGuid>{0B43679E-EDFA-4DA0-AD30-F4628B308B1B}</ProjectGuid>

View File

@@ -8,8 +8,10 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ImageResizer</RootNamespace>
<AssemblyName>ImageResizer.Test</AssemblyName>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,26 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Models
{
[TestClass]
public class CustomSizeTests
{
[TestMethod]
public void NameWorks()
{
var size = new CustomSize
{
Name = "Ignored",
};
Assert.AreEqual(Resources.Input_Custom, size.Name);
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ImageResizer.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -50,29 +51,14 @@ namespace ImageResizer.Models
Assert.AreEqual("OutputDir", result.DestinationDirectory);
}
/*[Fact]
public void Process_executes_in_parallel()
{
var batch = CreateBatch(_ => Thread.Sleep(50));
batch.Files.AddRange(
Enumerable.Range(0, Environment.ProcessorCount)
.Select(i => "Image" + i + ".jpg"));
var stopwatch = Stopwatch.StartNew();
batch.Process(CancellationToken.None, (_, __) => { });
stopwatch.Stop();
Assert.InRange(stopwatch.ElapsedMilliseconds, 50, 99);
}*/
[TestMethod]
public void ProcessAggregatesErrors()
public async Task ProcessAggregatesErrors()
{
var batch = CreateBatch(file => throw new InvalidOperationException("Error: " + file));
batch.Files.Add("Image1.jpg");
batch.Files.Add("Image2.jpg");
var errors = batch.Process((_, __) => { }, CancellationToken.None).ToList();
var errors = (await batch.ProcessAsync((_, __) => { }, CancellationToken.None)).ToList();
Assert.AreEqual(2, errors.Count);
@@ -91,14 +77,14 @@ namespace ImageResizer.Models
}
[TestMethod]
public void ProcessReportsProgress()
public async Task ProcessReportsProgress()
{
var batch = CreateBatch(_ => { });
batch.Files.Add("Image1.jpg");
batch.Files.Add("Image2.jpg");
var calls = new ConcurrentBag<(int I, double Count)>();
batch.Process(
await batch.ProcessAsync(
(i, count) => calls.Add((i, count)),
CancellationToken.None);
@@ -109,8 +95,12 @@ namespace ImageResizer.Models
{
var mock = new Mock<ResizeBatch> { CallBase = true };
mock.Protected()
.Setup("Execute", ItExpr.IsAny<string>(), ItExpr.IsAny<Settings>())
.Callback((string file, Settings settings) => executeAction(file));
.Setup<Task>("ExecuteAsync", ItExpr.IsAny<string>(), ItExpr.IsAny<Settings>())
.Returns((string file, Settings settings) =>
{
executeAction(file);
return Task.CompletedTask;
});
return mock.Object;
}

View File

@@ -7,10 +7,8 @@
using System;
using System.IO;
using System.Linq;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Threading.Tasks;
using ImageResizer.Extensions;
using ImageResizer.Properties;
using ImageResizer.Test;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -20,45 +18,59 @@ namespace ImageResizer.Models
[TestClass]
public class ResizeOperationTests : IDisposable
{
// Known legacy container format GUID for PNG, used as FallbackEncoder value in settings JSON
private static readonly Guid PngContainerFormatGuid = new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf");
private static readonly string[] DateTakenPropertyQuery = new[] { "System.Photo.DateTaken" };
private static readonly string[] CameraModelPropertyQuery = new[] { "System.Photo.CameraModel" };
private readonly TestDirectory _directory = new TestDirectory();
private bool disposedValue;
[TestMethod]
public void ExecuteCopiesFrameMetadata()
public async Task ExecuteCopiesFrameMetadata()
{
var operation = new ResizeOperation("Test.jpg", _directory, Settings());
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment));
async decoder =>
{
var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"), "Metadata should be preserved during transcode");
});
}
[TestMethod]
public void ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
public async Task ExecuteCopiesFrameMetadataEvenWhenMetadataCannotBeCloned()
{
var operation = new ResizeOperation("TestMetadataIssue2447.jpg", _directory, Settings());
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).CameraModel));
async decoder =>
{
var props = await decoder.BitmapProperties.GetPropertiesAsync(CameraModelPropertyQuery);
Assert.IsTrue(props.ContainsKey("System.Photo.CameraModel"), "Camera model metadata should be preserved");
});
}
[TestMethod]
public void ExecuteKeepsDateModified()
public async Task ExecuteKeepsDateModified()
{
var operation = new ResizeOperation("Test.png", _directory, Settings(s => s.KeepDateModified = true));
operation.Execute();
await operation.ExecuteAsync();
Assert.AreEqual(File.GetLastWriteTimeUtc("Test.png"), File.GetLastWriteTimeUtc(_directory.File()));
}
[TestMethod]
public void ExecuteKeepsDateModifiedWhenReplacingOriginals()
public async Task ExecuteKeepsDateModifiedWhenReplacingOriginals()
{
var path = Path.Combine(_directory, "Test.png");
File.Copy("Test.png", path);
@@ -75,55 +87,59 @@ namespace ImageResizer.Models
s.Replace = true;
}));
operation.Execute();
await operation.ExecuteAsync();
Assert.AreEqual(originalDateModified, File.GetLastWriteTimeUtc(_directory.File()));
}
[TestMethod]
public void ExecuteReplacesOriginals()
public async Task ExecuteReplacesOriginals()
{
var path = Path.Combine(_directory, "Test.png");
File.Copy("Test.png", path);
var operation = new ResizeOperation(path, null, Settings(s => s.Replace = true));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(_directory.File(), image => Assert.AreEqual(96, image.Frames[0].PixelWidth));
await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual(96u, decoder.PixelWidth));
}
[TestMethod]
public void ExecuteTransformsEachFrame()
public async Task ExecuteTransformsEachFrame()
{
var operation = new ResizeOperation("Test.gif", _directory, Settings());
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
async decoder =>
{
Assert.AreEqual(2, image.Frames.Count);
AssertEx.All(image.Frames, frame => Assert.AreEqual(96, frame.PixelWidth));
Assert.AreEqual(2u, decoder.FrameCount);
for (uint i = 0; i < decoder.FrameCount; i++)
{
var frame = await decoder.GetFrameAsync(i);
Assert.AreEqual(96u, frame.PixelWidth);
}
});
}
[TestMethod]
public void ExecuteUsesFallbackEncoder()
public async Task ExecuteUsesFallbackEncoder()
{
var operation = new ResizeOperation(
"Test.ico",
_directory,
Settings(s => s.FallbackEncoder = new PngBitmapEncoder().CodecInfo.ContainerFormat));
Settings(s => s.FallbackEncoder = PngContainerFormatGuid));
operation.Execute();
await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test).png");
}
[TestMethod]
public void TransformIgnoresOrientationWhenLandscapeToPortrait()
public async Task TransformIgnoresOrientationWhenLandscapeToPortrait()
{
var operation = new ResizeOperation(
"Test.png",
@@ -136,19 +152,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 192;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(192, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
Assert.AreEqual(192u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformIgnoresOrientationWhenPortraitToLandscape()
public async Task TransformIgnoresOrientationWhenPortraitToLandscape()
{
var operation = new ResizeOperation(
"TestPortrait.png",
@@ -161,19 +177,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(192, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(192u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformIgnoresIgnoreOrientationWhenAuto()
public async Task TransformIgnoresIgnoreOrientationWhenAuto()
{
var operation = new ResizeOperation(
"Test.png",
@@ -186,19 +202,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 0;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(48, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(48u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformIgnoresIgnoreOrientationWhenPercent()
public async Task TransformIgnoresIgnoreOrientationWhenPercent()
{
var operation = new ResizeOperation(
"Test.png",
@@ -213,19 +229,19 @@ namespace ImageResizer.Models
x.SelectedSize.Fit = ResizeFit.Stretch;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(192, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(192u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsShrinkOnly()
public async Task TransformHonorsShrinkOnly()
{
var operation = new ResizeOperation(
"Test.png",
@@ -238,19 +254,19 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 288;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(192, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
Assert.AreEqual(192u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformIgnoresShrinkOnlyWhenPercent()
public async Task TransformIgnoresShrinkOnlyWhenPercent()
{
var operation = new ResizeOperation(
"Test.png",
@@ -263,19 +279,19 @@ namespace ImageResizer.Models
x.SelectedSize.Unit = ResizeUnit.Percent;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(256, image.Frames[0].PixelWidth);
Assert.AreEqual(128, image.Frames[0].PixelHeight);
Assert.AreEqual(256u, decoder.PixelWidth);
Assert.AreEqual(128u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsShrinkOnlyWhenAutoHeight()
public async Task TransformHonorsShrinkOnlyWhenAutoHeight()
{
var operation = new ResizeOperation(
"Test.png",
@@ -288,15 +304,15 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 0;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.AreEqual(192, image.Frames[0].PixelWidth));
decoder => Assert.AreEqual(192u, decoder.PixelWidth));
}
[TestMethod]
public void TransformHonorsShrinkOnlyWhenAutoWidth()
public async Task TransformHonorsShrinkOnlyWhenAutoWidth()
{
var operation = new ResizeOperation(
"Test.png",
@@ -309,15 +325,15 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 288;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.AreEqual(96, image.Frames[0].PixelHeight));
decoder => Assert.AreEqual(96u, decoder.PixelHeight));
}
[TestMethod]
public void TransformHonorsUnit()
public async Task TransformHonorsUnit()
{
var operation = new ResizeOperation(
"Test.png",
@@ -330,82 +346,79 @@ namespace ImageResizer.Models
x.SelectedSize.Unit = ResizeUnit.Inch;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(_directory.File(), image => Assert.AreEqual(Math.Ceiling(image.Frames[0].DpiX), image.Frames[0].PixelWidth));
await AssertEx.ImageAsync(_directory.File(), decoder => Assert.AreEqual((uint)Math.Ceiling(decoder.DpiX), decoder.PixelWidth));
}
[TestMethod]
public void TransformHonorsFitWhenFit()
public async Task TransformHonorsFitWhenFit()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Fit));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(48, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(48u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsFitWhenFill()
public async Task TransformHonorsFitWhenFill()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Fill));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
async decoder =>
{
Assert.AreEqual(Colors.White, image.Frames[0].GetFirstPixel());
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
var pixel = await decoder.GetFirstPixelAsync();
Assert.AreEqual((byte)255, pixel.R, "First pixel R should be 255 (white)");
Assert.AreEqual((byte)255, pixel.G, "First pixel G should be 255 (white)");
Assert.AreEqual((byte)255, pixel.B, "First pixel B should be 255 (white)");
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsFitWhenStretch()
public async Task TransformHonorsFitWhenStretch()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(x => x.SelectedSize.Fit = ResizeFit.Stretch));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
async decoder =>
{
Assert.AreEqual(Colors.Black, image.Frames[0].GetFirstPixel());
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
var pixel = await decoder.GetFirstPixelAsync();
Assert.AreEqual((byte)0, pixel.R, "First pixel R should be 0 (black)");
Assert.AreEqual((byte)0, pixel.G, "First pixel G should be 0 (black)");
Assert.AreEqual((byte)0, pixel.B, "First pixel B should be 0 (black)");
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsFillWithShrinkOnlyWhenCropRequired()
public async Task TransformHonorsFillWithShrinkOnlyWhenCropRequired()
{
// Testing original 96x96 pixel Test.jpg cropped to 48x96 (Fill mode).
//
// ScaleX = 48/96 = 0.5
// ScaleY = 96/96 = 1.0
// Fill mode takes the max of these = 1.0.
//
// Previously, the transform logic saw the scale of 1.0 and returned the
// original dimensions. The corrected logic recognizes that a crop is
// required on one dimension and proceeds with the operation.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -417,22 +430,20 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(48, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
Assert.AreEqual(48u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
public async Task TransformHonorsFillWithShrinkOnlyWhenUpscaleAttempted()
{
// Confirm that attempting to upscale the original image will return the
// original dimensions when Shrink Only is enabled.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -444,21 +455,20 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 192;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
public async Task TransformHonorsFillWithShrinkOnlyWhenNoChangeRequired()
{
// With a scale of 1.0 on both axes, the original should be returned.
var operation = new ResizeOperation(
"Test.jpg",
_directory,
@@ -470,70 +480,70 @@ namespace ImageResizer.Models
x.SelectedSize.Height = 96;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image =>
decoder =>
{
Assert.AreEqual(96, image.Frames[0].PixelWidth);
Assert.AreEqual(96, image.Frames[0].PixelHeight);
Assert.AreEqual(96u, decoder.PixelWidth);
Assert.AreEqual(96u, decoder.PixelHeight);
});
}
[TestMethod]
public void GetDestinationPathUniquifiesOutputFilename()
public async Task GetDestinationPathUniquifiesOutputFilename()
{
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
var operation = new ResizeOperation("Test.png", _directory, Settings());
operation.Execute();
await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (1).png");
}
[TestMethod]
public void GetDestinationPathUniquifiesOutputFilenameAgain()
public async Task GetDestinationPathUniquifiesOutputFilenameAgain()
{
File.WriteAllBytes(Path.Combine(_directory, "Test (Test).png"), Array.Empty<byte>());
File.WriteAllBytes(Path.Combine(_directory, "Test (Test) (1).png"), Array.Empty<byte>());
var operation = new ResizeOperation("Test.png", _directory, Settings());
operation.Execute();
await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test (Test) (2).png");
}
[TestMethod]
public void GetDestinationPathUsesFileNameFormat()
public async Task GetDestinationPathUsesFileNameFormat()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(s => s.FileName = "%1_%2_%3_%4_%5_%6"));
operation.Execute();
await operation.ExecuteAsync();
CollectionAssert.Contains(_directory.FileNames.ToList(), "Test_Test_96_96_96_48.png");
}
[TestMethod]
public void ExecuteHandlesDirectoriesInFileNameFormat()
public async Task ExecuteHandlesDirectoriesInFileNameFormat()
{
var operation = new ResizeOperation(
"Test.png",
_directory,
Settings(s => s.FileName = @"Directory\%1 (%2)"));
operation.Execute();
await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test (Test).png"));
}
[TestMethod]
public void StripMetadata()
public async Task StripMetadata()
{
var operation = new ResizeOperation(
"TestMetadataIssue1928.jpg",
@@ -544,18 +554,26 @@ namespace ImageResizer.Models
x.RemoveMetadata = true;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
AssertEx.Image(
_directory.File(),
image => Assert.IsNotNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
async decoder =>
{
try
{
var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should be stripped");
}
catch (Exception)
{
// If GetPropertiesAsync throws, metadata is not present — which is expected
}
});
}
[TestMethod]
public void StripMetadataWhenNoMetadataPresent()
public async Task StripMetadataWhenNoMetadataPresent()
{
var operation = new ResizeOperation(
"TestMetadataIssue1928_NoMetadata.jpg",
@@ -566,18 +584,26 @@ namespace ImageResizer.Models
x.RemoveMetadata = true;
}));
operation.Execute();
await operation.ExecuteAsync();
AssertEx.Image(
await AssertEx.ImageAsync(
_directory.File(),
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).DateTaken));
AssertEx.Image(
_directory.File(),
image => Assert.IsNull(((BitmapMetadata)image.Frames[0].Metadata).GetQuerySafe("System.Photo.Orientation")));
async decoder =>
{
try
{
var props = await decoder.BitmapProperties.GetPropertiesAsync(DateTakenPropertyQuery);
Assert.IsFalse(props.ContainsKey("System.Photo.DateTaken"), "DateTaken should not exist");
}
catch (Exception)
{
// Expected: no metadata block at all
}
});
}
[TestMethod]
public void VerifyFileNameIsSanitized()
public async Task VerifyFileNameIsSanitized()
{
var operation = new ResizeOperation(
"Test.png",
@@ -589,13 +615,13 @@ namespace ImageResizer.Models
s.SelectedSize.Name = "Test\\/";
}));
operation.Execute();
await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\Directory\Test_______(Test__).png"));
}
[TestMethod]
public void VerifyNotRecommendedNameIsChanged()
public async Task VerifyNotRecommendedNameIsChanged()
{
var operation = new ResizeOperation(
"Test.png",
@@ -606,7 +632,7 @@ namespace ImageResizer.Models
s.FileName = @"nul";
}));
operation.Execute();
await operation.ExecuteAsync();
Assert.IsTrue(File.Exists(_directory + @"\nul_.png"));
}

View File

@@ -5,10 +5,8 @@
#pragma warning restore IDE0073
using System;
using System.Collections.Generic;
using System.ComponentModel;
using ImageResizer.Properties;
using ImageResizer.Test;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -31,27 +29,7 @@ namespace ImageResizer.Models
Assert.AreEqual(nameof(ResizeSize.Name), e.Arguments.PropertyName);
}
[TestMethod]
public void NameReplacesTokens()
{
var args = new List<(string, string)>
{
("$small$", Resources.Small),
("$medium$", Resources.Medium),
("$large$", Resources.Large),
("$phone$", Resources.Phone),
};
foreach (var (name, expected) in args)
{
var size = new ResizeSize
{
Name = name,
};
Assert.AreEqual(expected, size.Name);
}
}
// Note: NameReplacesTokens test removed - requires WinUI ResourceLoader runtime
[TestMethod]
public void FitWorks()
{

View File

@@ -25,9 +25,18 @@ namespace ImageResizer.Properties
WriteIndented = true,
};
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
// Cache the validation message format
private static CompositeFormat _valueMustBeBetween;
private static App _imageResizerApp;
private static CompositeFormat ValueMustBeBetweenFormat
{
get
{
// Use hardcoded string for test since ResourceLoader requires WinUI runtime
_valueMustBeBetween ??= System.Text.CompositeFormat.Parse("Value must be between '{0}' and '{1}'.");
return _valueMustBeBetween;
}
}
public SettingsTests()
{
@@ -38,8 +47,8 @@ namespace ImageResizer.Properties
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
// new App() needs to be created since Settings.Reload() uses App.Current to update properties on the UI thread. App() can be created only once otherwise it results in System.InvalidOperationException : Cannot create more than one System.Windows.Application instance in the same AppDomain.
_imageResizerApp = new App();
// Note: WinUI App cannot be instantiated in unit tests without the full WinUI runtime.
// Settings.Reload() has a fallback mechanism that allows it to work without a DispatcherQueue.
}
[TestMethod]
@@ -195,7 +204,7 @@ namespace ImageResizer.Properties
// Using InvariantCulture since this is used internally
Assert.AreEqual(
string.Format(CultureInfo.InvariantCulture, ValueMustBeBetween, 1, 100),
string.Format(CultureInfo.InvariantCulture, ValueMustBeBetweenFormat, 1, 100),
result);
}
@@ -385,8 +394,7 @@ namespace ImageResizer.Properties
[ClassCleanup]
public static void ClassCleanup()
{
_imageResizerApp.Dispose();
_imageResizerApp = null;
// No App instance to dispose in WinUI3 test environment
}
[TestCleanup]

View File

@@ -8,11 +8,14 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.IO.Abstractions;
using System.Windows.Media.Imaging;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Graphics.Imaging;
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1636:FileHeaderCopyrightTextMustMatch", Justification = "File created under PowerToys.")]
namespace ImageResizer.Test
@@ -29,17 +32,20 @@ namespace ImageResizer.Test
}
}
public static void Image(string path, Action<BitmapDecoder> action)
public static async Task ImageAsync(string path, Action<BitmapDecoder> action)
{
using (var stream = _fileSystem.File.OpenRead(path))
{
var image = BitmapDecoder.Create(
stream,
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.None);
using var stream = _fileSystem.File.OpenRead(path);
var winrtStream = stream.AsRandomAccessStream();
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
action(decoder);
}
action(image);
}
public static async Task ImageAsync(string path, Func<BitmapDecoder, Task> action)
{
using var stream = _fileSystem.File.OpenRead(path);
var winrtStream = stream.AsRandomAccessStream();
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
await action(decoder);
}
public static RaisedEvent<NotifyCollectionChangedEventArgs> Raises<T>(

View File

@@ -1,28 +1,34 @@
#pragma warning disable IDE0073
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace ImageResizer.Test
{
internal static class BitmapSourceExtensions
{
public static Color GetFirstPixel(this BitmapSource source)
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(this BitmapDecoder decoder)
{
var pixel = new byte[4];
new FormatConvertedBitmap(
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
PixelFormats.Bgra32,
destinationPalette: null,
alphaThreshold: 0)
.CopyPixels(pixel, 4, 0);
using var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied);
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
var buffer = new Windows.Storage.Streams.Buffer((uint)(softwareBitmap.PixelWidth * softwareBitmap.PixelHeight * 4));
softwareBitmap.CopyToBuffer(buffer);
using var reader = DataReader.FromBuffer(buffer);
byte b = reader.ReadByte();
byte g = reader.ReadByte();
byte r = reader.ReadByte();
byte a = reader.ReadByte();
return (r, g, b, a);
}
}
}

View File

@@ -1,50 +1,118 @@
#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
// 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.Properties;
using ImageResizer.Converters;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ImageResizer.Views
{
[TestClass]
public class TimeRemainingConverterTests
{
[DataTestMethod]
[DataRow("HourMinute", 1, 1, 0)]
[DataRow("HourMinutes", 1, 2, 0)]
[DataRow("HoursMinute", 2, 1, 0)]
[DataRow("HoursMinutes", 2, 2, 0)]
[DataRow("MinuteSecond", 0, 1, 1)]
[DataRow("MinuteSeconds", 0, 1, 2)]
[DataRow("MinutesSecond", 0, 2, 1)]
[DataRow("MinutesSeconds", 0, 2, 2)]
[DataRow("Second", 0, 0, 1)]
[DataRow("Seconds", 0, 0, 2)]
public void ConvertWorks(string resource, int hours, int minutes, int seconds)
[TestMethod]
public void Convert_ReturnsEmptyString_WhenTimeSpanIsMaxValue()
{
var timeRemaining = new TimeSpan(hours, minutes, seconds);
var converter = new TimeRemainingConverter();
// Using InvariantCulture since these are internal
var result = converter.Convert(
TimeSpan.MaxValue,
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void Convert_ReturnsEmptyString_WhenTotalSecondsLessThanOne()
{
var converter = new TimeRemainingConverter();
var result = converter.Convert(
TimeSpan.FromSeconds(0.5),
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void Convert_ReturnsEmptyString_WhenZeroTimeSpan()
{
var converter = new TimeRemainingConverter();
var result = converter.Convert(
TimeSpan.Zero,
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void Convert_ReturnsFormattedString_WhenValidTimeSpan()
{
var converter = new TimeRemainingConverter();
var timeRemaining = new TimeSpan(0, 5, 30);
var result = converter.Convert(
timeRemaining,
targetType: null,
parameter: null,
CultureInfo.InvariantCulture);
language: string.Empty);
Assert.AreEqual(
string.Format(
CultureInfo.InvariantCulture,
Resources.ResourceManager.GetString("Progress_TimeRemaining_" + resource, CultureInfo.InvariantCulture),
hours,
minutes,
seconds),
result);
// The result should contain the time remaining information
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(string));
Assert.AreNotEqual(string.Empty, result);
}
[TestMethod]
public void Convert_ReturnsEmptyString_WhenValueIsNotTimeSpan()
{
var converter = new TimeRemainingConverter();
var result = converter.Convert(
"not a timespan",
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void Convert_ReturnsEmptyString_WhenValueIsNull()
{
var converter = new TimeRemainingConverter();
var result = converter.Convert(
null,
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void ConvertBack_ReturnsValueUnchanged()
{
var converter = new TimeRemainingConverter();
var input = "test value";
var result = converter.ConvertBack(
input,
targetType: null,
parameter: null,
language: string.Empty);
Assert.AreEqual(input, result);
}
}
}

View File

@@ -1,28 +0,0 @@
<Application
x:Class="ImageResizer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:ImageResizer.Models"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:v="clr-namespace:ImageResizer.Views">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
<v:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
<v:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
<v:EnumValueConverter x:Key="EnumValueConverter" />
<v:AutoDoubleConverter x:Key="AutoDoubleConverter" />
<v:BoolValueConverter x:Key="BoolValueConverter" />
<v:VisibilityBoolConverter x:Key="VisibilityBoolConverter" />
<v:EnumToIntConverter x:Key="EnumToIntConverter" />
<v:AccessTextToTextConverter x:Key="AccessTextToTextConverter" />
<v:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
<v:ZeroToEmptyStringNumberFormatter x:Key="ZeroToEmptyStringNumberFormatter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,243 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using ImageResizer.Models;
using ImageResizer.Properties;
using ImageResizer.Utilities;
using ImageResizer.ViewModels;
using ImageResizer.Views;
using ManagedCommon;
namespace ImageResizer
{
public partial class App : Application, IDisposable
{
private const string LogSubFolder = "\\Image Resizer\\Logs";
/// <summary>
/// Gets cached AI availability state, checked at app startup.
/// Can be updated after model download completes or background initialization.
/// </summary>
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
/// <summary>
/// Event fired when AI initialization completes in background.
/// Allows UI to refresh state when initialization finishes.
/// </summary>
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
static App()
{
try
{
// Initialize logger early (mirroring PowerOCR pattern)
Logger.InitializeLogger(LogSubFolder);
}
catch
{
/* swallow logger init issues silently */
}
try
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
}
}
catch (CultureNotFoundException ex)
{
Logger.LogError("CultureNotFoundException: " + ex.Message);
}
Console.InputEncoding = Encoding.Unicode;
}
protected override void OnStartup(StartupEventArgs e)
{
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
NativeMethods.SetProcessDPIAware();
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
#if true // Set to false to re-enable AI Super Resolution
AiAvailabilityState = AiAvailabilityState.NotSupported;
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
// Skip AI detection mode as well
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
{
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
Environment.Exit(0);
return;
}
#else
// Check for AI detection mode (called by Runner in background)
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
{
RunAiDetectionMode();
return;
}
#endif
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
{
/* TODO: Add logs to ImageResizer.
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
*/
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
return;
}
// AI Super Resolution is not supported on Windows 10 - skip cache check entirely
if (OSVersionHelper.IsWindows10())
{
AiAvailabilityState = AiAvailabilityState.NotSupported;
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
}
else
{
// Load AI availability from cache (written by Runner's background detection)
var cachedState = Services.AiAvailabilityCacheService.LoadCache();
if (cachedState.HasValue)
{
AiAvailabilityState = cachedState.Value;
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
}
else
{
// No valid cache - default to NotSupported (Runner will detect and cache for next startup)
AiAvailabilityState = AiAvailabilityState.NotSupported;
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
}
// If AI is potentially available, start background initialization (non-blocking)
if (AiAvailabilityState == AiAvailabilityState.Ready)
{
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
}
else
{
// AI not available - set NoOp service immediately
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
}
}
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
mainWindow.Show();
// Temporary workaround for issue #1273
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
}
/// <summary>
/// AI detection mode: perform detection, write to cache, and exit.
/// Called by Runner in background to avoid blocking ImageResizer UI startup.
/// </summary>
private void RunAiDetectionMode()
{
try
{
Logger.LogInfo("Running AI detection mode...");
// AI Super Resolution is not supported on Windows 10
if (OSVersionHelper.IsWindows10())
{
Logger.LogInfo("AI detection skipped: Windows 10 does not support AI Super Resolution");
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
Environment.Exit(0);
return;
}
// Perform detection (reuse existing logic)
var state = CheckAiAvailability();
// Write result to cache file
Services.AiAvailabilityCacheService.SaveCache(state);
Logger.LogInfo($"AI detection complete: {state}");
}
catch (Exception ex)
{
Logger.LogError($"AI detection failed: {ex.Message}");
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
}
// Exit silently without showing UI
Environment.Exit(0);
}
/// <summary>
/// Check AI Super Resolution availability on this system.
/// Performs architecture check and model availability check.
/// </summary>
private static AiAvailabilityState CheckAiAvailability()
{
// AI feature disabled - always return NotSupported
return AiAvailabilityState.NotSupported;
}
/// <summary>
/// Initialize AI Super Resolution service asynchronously in background.
/// Runs without blocking UI startup - state change event notifies completion.
/// </summary>
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
{
AiAvailabilityState finalState;
try
{
// Create and initialize AI service using async factory
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
if (aiService != null)
{
ResizeBatch.SetAiSuperResolutionService(aiService);
Logger.LogInfo("AI Super Resolution service initialized successfully.");
finalState = AiAvailabilityState.Ready;
}
else
{
// Initialization failed - use default NoOp service
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
finalState = AiAvailabilityState.NotSupported;
}
}
catch (Exception ex)
{
// Log error and use default NoOp service
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
finalState = AiAvailabilityState.NotSupported;
}
// Update cached state and notify listeners
AiAvailabilityState = finalState;
AiInitializationCompleted?.Invoke(null, finalState);
}
public void Dispose()
{
// Dispose AI Super Resolution service
ResizeBatch.DisposeAiSuperResolutionService();
GC.SuppressFinalize(this);
}
}
}

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -6,6 +6,7 @@ using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ImageResizer.Models;
using ImageResizer.Properties;
@@ -58,10 +59,10 @@ namespace ImageResizer.Cli
return 1;
}
return RunSilentMode(cliOptions);
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
}
private int RunSilentMode(CliOptions cliOptions)
private async Task<int> RunSilentModeAsync(CliOptions cliOptions)
{
var batch = ResizeBatch.FromCliOptions(Console.In, cliOptions);
var settings = Settings.Default;
@@ -73,7 +74,7 @@ namespace ImageResizer.Cli
bool useLineBasedProgress = cliOptions.ProgressLines ?? false;
int lastReportedMilestone = -1;
var errors = batch.Process(
var errors = await batch.ProcessAsync(
(completed, total) =>
{
var progress = (int)((completed / total) * 100);

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
: base(_aliases[0], Properties.Resources.CLI_Option_Destination)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--filename", "-n"];
public FileNameOption()
: base(_aliases, Properties.Resources.CLI_Option_FileName)
: base(_aliases[0], Properties.Resources.CLI_Option_FileName)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--fit", "-f"];
public FitOption()
: base(_aliases, Properties.Resources.CLI_Option_Fit)
: base(_aliases[0], Properties.Resources.CLI_Option_Fit)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--height", "-h"];
public HeightOption()
: base(_aliases, Properties.Resources.CLI_Option_Height)
: base(_aliases[0], Properties.Resources.CLI_Option_Height)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--help", "-?", "/?"];
public HelpOption()
: base(_aliases, Properties.Resources.CLI_Option_Help)
: base(_aliases[0], Properties.Resources.CLI_Option_Help)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--ignore-orientation"];
public IgnoreOrientationOption()
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
: base(_aliases[0], Properties.Resources.CLI_Option_IgnoreOrientation)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--keep-date-modified"];
public KeepDateModifiedOption()
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
: base(_aliases[0], Properties.Resources.CLI_Option_KeepDateModified)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
public ProgressLinesOption()
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
: base(_aliases[0], "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--quality", "-q"];
public QualityOption()
: base(_aliases, Properties.Resources.CLI_Option_Quality)
: base(_aliases[0], Properties.Resources.CLI_Option_Quality)
{
AddValidator(result =>
{

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--remove-metadata"];
public RemoveMetadataOption()
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
: base(_aliases[0], Properties.Resources.CLI_Option_RemoveMetadata)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--replace", "-r"];
public ReplaceOption()
: base(_aliases, Properties.Resources.CLI_Option_Replace)
: base(_aliases[0], Properties.Resources.CLI_Option_Replace)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--show-config", "--config"];
public ShowConfigOption()
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
: base(_aliases[0], Properties.Resources.CLI_Option_ShowConfig)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--shrink-only"];
public ShrinkOnlyOption()
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
: base(_aliases[0], Properties.Resources.CLI_Option_ShrinkOnly)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--size"];
public SizeOption()
: base(_aliases, Properties.Resources.CLI_Option_Size)
: base(_aliases[0], Properties.Resources.CLI_Option_Size)
{
AddValidator(result =>
{

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--unit", "-u"];
public UnitOption()
: base(_aliases, Properties.Resources.CLI_Option_Unit)
: base(_aliases[0], Properties.Resources.CLI_Option_Unit)
{
}
}

View File

@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
private static readonly string[] _aliases = ["--width", "-w"];
public WidthOption()
: base(_aliases, Properties.Resources.CLI_Option_Width)
: base(_aliases[0], Properties.Resources.CLI_Option_Width)
{
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml.Data;
namespace ImageResizer.Converters
{
public partial class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Enum)
{
return System.Convert.ToInt32(value);
}
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is int intValue && targetType.IsEnum)
{
return Enum.ToObject(targetType, intValue);
}
return value;
}
}
}

View File

@@ -1,26 +1,21 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// 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/
#pragma warning restore IDE0073
using System;
using System.Globalization;
using System.Text;
using System.Windows.Data;
using ImageResizer.Helpers;
using Microsoft.UI.Xaml.Data;
using ImageResizer.Properties;
namespace ImageResizer.Views
namespace ImageResizer.Converters
{
[ValueConversion(typeof(Enum), typeof(string))]
public class EnumValueConverter : IValueConverter
public partial class EnumValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object value, Type targetType, object parameter, string language)
{
var type = value?.GetType();
if (!type.IsEnum)
if (type == null || !type.IsEnum)
{
return value;
}
@@ -44,20 +39,18 @@ namespace ImageResizer.Views
.Append(parameter);
}
// Fixes #16792 - Looks like culture defaults to en-US, so wrong resource is being fetched.
#pragma warning disable CA1304 // Specify CultureInfo
var targetValue = Resources.ResourceManager.GetString(builder.ToString());
#pragma warning restore CA1304 // Specify CultureInfo
var targetValue = ResourceLoaderInstance.ResourceLoader.GetString(builder.ToString());
if (toLower)
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, CultureInfo culture)
public object ConvertBack(object value, Type targetType, object parameter, string language)
=> value;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml.Data;
namespace ImageResizer.Converters
{
public partial class 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,
};
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,25 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
namespace System.Windows.Media.Imaging
{
internal static class BitmapEncoderExtensions
{
public static bool CanEncode(this BitmapEncoder encoder)
{
try
{
var temp = encoder.CodecInfo;
}
catch (NotSupportedException)
{
return false;
}
return true;
}
}
}

View File

@@ -1,258 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Media.Imaging;
namespace ImageResizer.Extensions
{
internal static class BitmapMetadataExtension
{
public static void CopyMetadataPropertyTo(this BitmapMetadata source, BitmapMetadata target, string query)
{
if (source == null || target == null || string.IsNullOrWhiteSpace(query))
{
return;
}
try
{
var value = source.GetQuerySafe(query);
if (value == null)
{
return;
}
target.SetQuery(query, value);
}
catch (InvalidOperationException)
{
// InvalidOperationException is thrown if metadata object is in readonly state.
return;
}
}
public static object GetQuerySafe(this BitmapMetadata metadata, string query)
{
if (metadata == null || string.IsNullOrWhiteSpace(query))
{
return null;
}
try
{
if (metadata.ContainsQuery(query))
{
return metadata.GetQuery(query);
}
else
{
return null;
}
}
catch (NotSupportedException)
{
// NotSupportedException is throw if the metadata entry is not preset on the target image (e.g. Orientation not set).
return null;
}
}
public static void RemoveQuerySafe(this BitmapMetadata metadata, string query)
{
if (metadata == null || string.IsNullOrWhiteSpace(query))
{
return;
}
try
{
if (metadata.ContainsQuery(query))
{
metadata.RemoveQuery(query);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Exception while trying to remove metadata entry at position: {query}");
Debug.WriteLine(ex);
}
}
public static void SetQuerySafe(this BitmapMetadata metadata, string query, object value)
{
if (metadata == null || string.IsNullOrWhiteSpace(query) || value == null)
{
return;
}
try
{
metadata.SetQuery(query, value);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception while trying to set metadata {value} at position: {query}");
Debug.WriteLine(ex);
}
}
/// <summary>
/// Gets all metadata.
/// Iterates recursively through metadata and adds valid items to a list while skipping invalid data items.
/// </summary>
/// <remarks>
/// Invalid data items are items which throw an exception when reading the data with metadata.GetQuery(...).
/// Sometimes Metadata collections are improper closed and cause an exception on IEnumerator.MoveNext(). In this case, we return all data items which were successfully collected so far.
/// </remarks>
/// <returns>
/// metadata path and metadata value of all successfully read data items.
/// </returns>
public static List<(string MetadataPath, object Value)> GetListOfMetadata(this BitmapMetadata metadata)
{
var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
try
{
GetMetadataRecursively(metadata, string.Empty);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
Debug.WriteLine(ex);
}
return listOfAllMetadata;
void GetMetadataRecursively(BitmapMetadata metadata, string query)
{
foreach (string relativeQuery in metadata)
{
string absolutePath = query + relativeQuery;
object metadataQueryReader = null;
try
{
metadataQueryReader = GetQueryWithPreCheck(metadata, relativeQuery);
}
catch (Exception ex)
{
Debug.WriteLine($"Removing corrupt metadata property {absolutePath}. Skipping metadata entry | {ex.Message}");
Debug.WriteLine(ex);
}
if (metadataQueryReader != null)
{
listOfAllMetadata.Add((absolutePath, metadataQueryReader));
}
else
{
Debug.WriteLine($"No metadata found for query {absolutePath}. Skipping empty null entry because its invalid.");
}
if (metadataQueryReader is BitmapMetadata innerMetadata)
{
GetMetadataRecursively(innerMetadata, absolutePath);
}
}
}
object GetQueryWithPreCheck(BitmapMetadata metadata, string query)
{
if (metadata == null || string.IsNullOrWhiteSpace(query))
{
return null;
}
if (metadata.ContainsQuery(query))
{
return metadata.GetQuery(query);
}
else
{
return null;
}
}
}
/// <summary>
/// Prints all metadata to debug console
/// </summary>
/// <remarks>
/// Intended for debug only!!!
/// </remarks>
public static void PrintsAllMetadataToDebugOutput(this BitmapMetadata metadata)
{
if (metadata == null)
{
Debug.WriteLine($"Metadata was null.");
}
var listOfMetadata = metadata.GetListOfMetadataForDebug();
foreach (var metadataItem in listOfMetadata)
{
// Debug.WriteLine($"modifiableMetadata.RemoveQuerySafe(\"{metadataItem.metadataPath}\");");
Debug.WriteLine($"{metadataItem.MetadataPath} | {metadataItem.Value}");
}
}
/// <summary>
/// Gets all metadata
/// Iterates recursively through all metadata
/// </summary>
/// <remarks>
/// Intended for debug only!!!
/// </remarks>
public static List<(string MetadataPath, object Value)> GetListOfMetadataForDebug(this BitmapMetadata metadata)
{
var listOfAllMetadata = new List<(string MetadataPath, object Value)>();
try
{
GetMetadataRecursively(metadata, string.Empty);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception while trying to iterate recursively over metadata. We were able to read {listOfAllMetadata.Count} metadata entries.");
Debug.WriteLine(ex);
}
return listOfAllMetadata;
void GetMetadataRecursively(BitmapMetadata metadata, string query)
{
if (metadata == null)
{
return;
}
foreach (string relativeQuery in metadata)
{
string absolutePath = query + relativeQuery;
object metadataQueryReader = null;
try
{
metadataQueryReader = metadata.GetQuerySafe(relativeQuery);
listOfAllMetadata.Add((absolutePath, metadataQueryReader));
}
catch (Exception ex)
{
listOfAllMetadata.Add((absolutePath, $"######## INVALID METADATA: {ex.Message}"));
Debug.WriteLine(ex);
}
if (metadataQueryReader is BitmapMetadata innerMetadata)
{
GetMetadataRecursively(innerMetadata, absolutePath);
}
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ImageResizer.Helpers
{
public class Observable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -1,61 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Input;
namespace ImageResizer.Helpers
{
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action execute)
: this(execute, null)
{
}
public RelayCommand(Action execute, Func<bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
public void Execute(object parameter) => _execute();
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")]
public class RelayCommand<T> : ICommand
{
private readonly Action<T> execute;
private readonly Func<T, bool> canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
}
public bool CanExecute(object parameter) => canExecute == null || canExecute((T)parameter);
public void Execute(object parameter) => execute((T)parameter);
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Windows.ApplicationModel.Resources;
namespace ImageResizer.Helpers
{
internal static class ResourceLoaderInstance
{
internal static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
}
}
}

View File

@@ -1,82 +1,92 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<UseWPF>true</UseWPF>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
</PropertyGroup>
<PropertyGroup>
<ProjectGuid>{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}</ProjectGuid>
<AssemblyDescription>PowerToys Image Resizer</AssemblyDescription>
<OutputType>WinExe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<RootNamespace>ImageResizer</RootNamespace>
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ApplicationManifest>app.manifest</ApplicationManifest>
<UseWinUI>true</UseWinUI>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>CA1863</NoWarn>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
<!-- 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>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
<NoWarn>0436;SA1210;SA1516;CA1305;CA1863;CA1852</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
<!-- Allow test project to access internal types -->
<ItemGroup>
<InternalsVisibleTo Include="ImageResizer.Test" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\ImageResizer\ImageResizer.ico" />
</ItemGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\ImageResizer.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\ImageResizer.png" />
</ItemGroup>
<ItemGroup>
<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.Wpf" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="WPF-UI" />
<PackageReference Include="WinUIEx" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!-- 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>
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<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>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<!-- Ensure Resources directory and ImageResizer.png are available for dependent projects -->
<Target Name="CopyResourcesToSharedLocation" AfterTargets="Build">
<ItemGroup>
<ResourceFiles Include="$(MSBuildProjectDirectory)\Resources\ImageResizer.png" />
</ItemGroup>
<MakeDir Directories="$(OutputPath)Resources" Condition="!Exists('$(OutputPath)Resources')" />
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(OutputPath)Resources" SkipUnchangedFiles="true" />
</Target>
</Project>
</Project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.ImageResizerUI" />
</assembly>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
packageName="Microsoft.PowerToys.SparseApp"
applicationId="PowerToys.ImageResizerUI" />
</assembly>

View 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:converters="using:ImageResizer.Converters"
xmlns:local="using:ImageResizer"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<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"
FalseValue="Visible"
TrueValue="Collapsed" />
</ResourceDictionary>
</Application.Resources>
</Application>

View 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);
}
}
}

View File

@@ -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="400"
Height="506"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
mc:Ignorable="d">
<ContentPresenter x:Name="contentPresenter" />
</winuiEx:WindowEx>

View File

@@ -0,0 +1,211 @@
// 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;
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; }
private PropertyChangedEventHandler _selectedSizeChangedHandler;
private InputViewModel _currentInputViewModel;
// Window chrome height (title bar)
private const double WindowChromeHeight = 32;
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;
// Size to content after layout
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
}
else if (page is ResultsViewModel resultsVM)
{
var resultsPage = new ResultsPage { ViewModel = resultsVM, DataContext = resultsVM };
contentPresenter.Content = resultsPage;
// Size to content after layout
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
}
}
private void AdjustWindowHeightForInputPage(InputViewModel inputVM)
{
// Unsubscribe previous handler to prevent memory leak
if (_selectedSizeChangedHandler != null && _currentInputViewModel?.Settings != null)
{
_currentInputViewModel.Settings.PropertyChanged -= _selectedSizeChangedHandler;
}
_currentInputViewModel = inputVM;
// Create and store handler reference for future cleanup
_selectedSizeChangedHandler = (s, e) =>
{
if (e.PropertyName == nameof(inputVM.Settings.SelectedSize))
{
// Delay to allow layout to update
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
}
};
inputVM.Settings.PropertyChanged += _selectedSizeChangedHandler;
// Set initial height after layout
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
}
private void SizeToContent()
{
// Get the content element
var content = contentPresenter.Content as FrameworkElement;
if (content == null)
{
return;
}
// Measure the content to get its desired size
content.Measure(new Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredHeight = content.DesiredSize.Height;
if (desiredHeight <= 0)
{
return;
}
// Add window chrome height and extra padding for safety
var totalHeight = desiredHeight + WindowChromeHeight + 16;
var appWindow = this.AppWindow;
if (appWindow != null)
{
var scaleFactor = Content?.XamlRoot?.RasterizationScale ?? 1.0;
var currentSize = appWindow.Size;
var newHeightInPixels = (int)(totalHeight * scaleFactor);
appWindow.Resize(new Windows.Graphics.SizeInt32(currentSize.Width, newHeightInPixels));
}
}
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);
}
}
}

View File

@@ -1,8 +1,7 @@
#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
// 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;
@@ -10,8 +9,8 @@ namespace ImageResizer.Views
{
public interface IMainView
{
void Close();
IEnumerable<string> OpenPictureFiles();
void Close();
}
}

View File

@@ -0,0 +1,336 @@
<!-- 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 Grid.Row="0" 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}}">
<!-- TODO: Add ProgressRing here -->
<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
Grid.Row="0"
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="&#xE799;" />
<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="&#xE7A0;"
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"
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{Binding Settings.SelectedSize.Height, Mode=TwoWay}" />
<FontIcon
Grid.Row="2"
VerticalAlignment="Center"
FontSize="20"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Glyph="&#xE7A8;" />
<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="&#xECC6;" />
<ComboBox
Grid.Row="2"
Grid.Column="4"
Margin="8,0,0,0"
HorizontalAlignment="Stretch"
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="&#xE713;" />
<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="&#xE740;" />
<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>

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// 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);
}
}
}

View File

@@ -1,18 +1,27 @@
<UserControl
<!-- 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:p="clr-namespace:ImageResizer.Properties">
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"
Text="{x:Static p:Resources.Results_MainInstruction}" />
<ScrollViewer HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors}">
FontSize="16" />
<ScrollViewer
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalScrollBarVisibility="Auto">
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="ResizeError">
<DataTemplate>
<StackPanel>
<TextBlock
Margin="0,8,0,0"
@@ -27,18 +36,15 @@
<Border
Margin="0,12,0,0"
Padding="12,12"
Background="{DynamicResource LayerFillColorDefaultBrush}"
BorderBrush="{DynamicResource DividerStrokeColorDefaultBrush}"
Padding="12"
Background="{StaticResource LayerFillColorDefaultBrush}"
BorderBrush="{StaticResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
MinWidth="76"
Command="{Binding CloseCommand}"
Content="{x:Static p:Resources.Results_Close}"
IsCancel="True"
IsDefault="True" />
<Button MinWidth="76" Command="{Binding CloseCommand}">
<TextBlock x:Uid="Results_Close" />
</Button>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
</Page>

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,32 +1,41 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Text;
using System.Text.Json.Serialization;
using ImageResizer.Properties;
using CommunityToolkit.Mvvm.ComponentModel;
using ImageResizer.Helpers;
namespace ImageResizer.Models
{
public class AiSize : ResizeSize
public partial class AiSize : ResizeSize
{
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
private static CompositeFormat _scaleFormat;
private static CompositeFormat ScaleFormat
{
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., "2×").
/// Gets the formatted scale display string (e.g., "2x").
/// </summary>
[JsonIgnore]
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
[JsonPropertyName("scale")]
public int Scale
{
get => _scale;
set => Set(ref _scale, value);
}
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, Scale);
[JsonConstructor]
public AiSize(int scale)

View File

@@ -8,6 +8,7 @@ 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
@@ -19,117 +20,51 @@ namespace ImageResizer.Models
/// </summary>
public class CliOptions
{
/// <summary>
/// Gets or sets a value indicating whether to show help information.
/// </summary>
public bool ShowHelp { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show current configuration.
/// </summary>
public bool ShowConfig { get; set; }
/// <summary>
/// Gets or sets the destination directory for resized images.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// Gets or sets the width of the resized image.
/// </summary>
public double? Width { get; set; }
/// <summary>
/// Gets or sets the height of the resized image.
/// </summary>
public double? Height { get; set; }
/// <summary>
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
/// </summary>
public ResizeUnit? Unit { get; set; }
/// <summary>
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
/// </summary>
public ResizeFit? Fit { get; set; }
/// <summary>
/// Gets or sets the index of the preset size to use.
/// </summary>
public int? SizeIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
/// </summary>
public bool? ShrinkOnly { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to replace the original file.
/// </summary>
public bool? Replace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore orientation when resizing.
/// </summary>
public bool? IgnoreOrientation { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remove metadata from the resized image.
/// </summary>
public bool? RemoveMetadata { get; set; }
/// <summary>
/// Gets or sets the JPEG quality level (1-100).
/// </summary>
public int? JpegQualityLevel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to keep the date modified.
/// </summary>
public bool? KeepDateModified { get; set; }
/// <summary>
/// Gets or sets the output filename format.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
/// </summary>
public bool? ProgressLines { get; set; }
/// <summary>
/// Gets the list of files to process.
/// </summary>
public ICollection<string> Files { get; } = new List<string>();
/// <summary>
/// Gets or sets the pipe name for receiving file list.
/// </summary>
public string PipeName { get; set; }
/// <summary>
/// Gets parse/validation errors produced by System.CommandLine.
/// </summary>
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
/// <summary>
/// Converts a boolean value to nullable bool (true -> true, false -> null).
/// </summary>
private static bool? ToBoolOrNull(bool value) => value ? true : null;
/// <summary>
/// Parses command-line arguments into CliOptions using System.CommandLine.
/// </summary>
/// <param name="args">The command-line arguments.</param>
/// <returns>A CliOptions instance with parsed values.</returns>
public static CliOptions Parse(string[] args)
{
var options = new CliOptions();
var cmd = new ImageResizerRootCommand();
// Parse using System.CommandLine
var parseResult = new Parser(cmd).Parse(args);
if (parseResult.Errors.Count > 0)
@@ -143,7 +78,6 @@ namespace ImageResizer.Models
options.ParseErrors = new ReadOnlyCollection<string>(errors);
}
// Extract values from parse result using strongly typed options
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
@@ -153,7 +87,6 @@ namespace ImageResizer.Models
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
// Convert bool to nullable bool (true -> true, false -> null)
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
@@ -165,14 +98,12 @@ namespace ImageResizer.Models
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
// Get files from arguments
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
if (files != null)
{
const string pipeNamePrefix = "\\\\.\\pipe\\";
foreach (var file in files)
{
// Check for pipe name (must be at the start of the path)
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
{
options.PipeName = file.Substring(pipeNamePrefix.Length);
@@ -187,62 +118,55 @@ namespace ImageResizer.Models
return options;
}
/// <summary>
/// Prints current configuration to the console.
/// </summary>
/// <param name="settings">The settings to display.</param>
public static void PrintConfig(ImageResizer.Properties.Settings settings)
{
var loader = ResourceLoaderInstance.ResourceLoader;
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
Console.WriteLine(loader.GetString("CLI_ConfigTitle"));
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
Console.WriteLine(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(Properties.Resources.CLI_ConfigCustomSize);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
Console.WriteLine(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(Properties.Resources.CLI_ConfigPresetSizes);
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, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
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, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigCustomSelected"), settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
}
}
/// <summary>
/// Prints usage information to the console.
/// </summary>
public static void PrintUsage()
{
var loader = ResourceLoaderInstance.ResourceLoader;
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
Console.WriteLine(loader.GetString("CLI_UsageTitle"));
Console.WriteLine();
var cmd = new ImageResizerRootCommand();
// Print usage line
Console.WriteLine(Properties.Resources.CLI_UsageLine);
Console.WriteLine(loader.GetString("CLI_UsageLine"));
Console.WriteLine();
// Print options from the command definition
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
Console.WriteLine(loader.GetString("CLI_UsageOptions"));
foreach (var option in cmd.Options)
{
var aliases = string.Join(", ", option.Aliases);
@@ -251,11 +175,11 @@ namespace ImageResizer.Models
}
Console.WriteLine();
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
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"));
}
}
}

View File

@@ -1,12 +1,11 @@
#pragma warning disable IDE0073
#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.Properties;
using ImageResizer.Helpers;
namespace ImageResizer.Models
{
@@ -15,7 +14,7 @@ namespace ImageResizer.Models
[JsonIgnore]
public override string Name
{
get => Resources.Input_Custom;
get => ResourceLoaderInstance.ResourceLoader.GetString("Input_Custom");
set { /* no-op */ }
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ImageResizer.Models
{
/// <summary>
/// PNG interlace option for the encoder.
/// Integer values preserve backward compatibility with existing settings JSON.
/// </summary>
public enum PngInterlaceOption
{
Default = 0,
On = 1,
Off = 2,
}
/// <summary>
/// TIFF compression option for the encoder.
/// Integer values preserve backward compatibility with existing settings JSON.
/// </summary>
public enum TiffCompressOption
{
Default = 0,
None = 1,
Ccitt3 = 2,
Ccitt4 = 3,
Lzw = 4,
Rle = 5,
Zip = 6,
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/
@@ -14,7 +14,6 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ImageResizer.Properties;
using ImageResizer.Services;
@@ -141,33 +140,31 @@ namespace ImageResizer.Models
return FromCliOptions(standardInput, options);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, CancellationToken cancellationToken)
public Task<IEnumerable<ResizeError>> ProcessAsync(Action<int, double> reportProgress, CancellationToken cancellationToken)
{
// NOTE: Settings.Default is captured once before parallel processing.
// 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);
return ProcessAsync(reportProgress, Settings.Default, cancellationToken);
}
public IEnumerable<ResizeError> Process(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
public async Task<IEnumerable<ResizeError>> ProcessAsync(Action<int, double> reportProgress, Settings settings, CancellationToken cancellationToken)
{
double total = Files.Count;
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(
await Parallel.ForEachAsync(
Files,
new ParallelOptions
{
CancellationToken = cancellationToken,
},
(file, state, i) =>
async (file, ct) =>
{
try
{
Execute(file, settings);
await ExecuteAsync(file, settings);
}
catch (Exception ex)
{
@@ -181,10 +178,10 @@ namespace ImageResizer.Models
return errors;
}
protected virtual void Execute(string file, Settings settings)
protected virtual async Task ExecuteAsync(string file, Settings settings)
{
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
await new ResizeOperation(file, DestinationDirectory, settings, aiService).ExecuteAsync();
}
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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.

View File

@@ -1,26 +1,24 @@
#pragma warning disable IDE0073
#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 System.Threading.Tasks;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
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
@@ -35,7 +33,20 @@ namespace ImageResizer.Models
private readonly IAISuperResolutionService _aiSuperResolutionService;
// Cache CompositeFormat for AI error message formatting (CA1863)
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
private static CompositeFormat _aiErrorFormat;
private static CompositeFormat AiErrorFormat
{
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 =
@@ -53,78 +64,134 @@ namespace ImageResizer.Models
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
}
public void Execute()
public async Task ExecuteAsync()
{
string path;
using (var inputStream = _fileSystem.File.OpenRead(_file))
{
var decoder = BitmapDecoder.Create(
inputStream,
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.None);
var winrtInputStream = inputStream.AsRandomAccessStream();
var decoder = await BitmapDecoder.CreateAsync(winrtInputStream);
var containerFormat = decoder.CodecInfo.ContainerFormat;
var encoder = CreateEncoder(containerFormat);
if (decoder.Metadata != null)
// Determine encoder ID from decoder
var encoderId = CodecHelper.GetEncoderIdForDecoder(decoder);
if (encoderId == null || !CodecHelper.CanEncode(encoderId.Value))
{
try
{
encoder.Metadata = decoder.Metadata;
}
catch (InvalidOperationException)
{
}
encoderId = CodecHelper.GetEncoderIdFromLegacyGuid(_settings.FallbackEncoder);
}
if (decoder.Palette != null)
var encoderGuid = encoderId.Value;
if (_settings.SelectedSize is AiSize)
{
encoder.Palette = decoder.Palette;
// AI super resolution path
path = await ExecuteAiAsync(decoder, winrtInputStream, encoderGuid);
}
foreach (var originalFrame in decoder.Frames)
else
{
var transformedBitmap = Transform(originalFrame);
// Standard resize path
var originalWidth = (int)decoder.PixelWidth;
var originalHeight = (int)decoder.PixelHeight;
var dpiX = decoder.DpiX;
var dpiY = decoder.DpiY;
// if the frame was not modified, we should not replace the metadata
if (transformedBitmap == originalFrame)
var (scaledWidth, scaledHeight, cropBounds, noTransformNeeded) =
CalculateDimensions(originalWidth, originalHeight, dpiX, dpiY);
// For destination path, calculate final output dimensions
int outputWidth, outputHeight;
if (noTransformNeeded)
{
encoder.Frames.Add(originalFrame);
outputWidth = originalWidth;
outputHeight = originalHeight;
}
else if (cropBounds.HasValue)
{
outputWidth = (int)cropBounds.Value.Width;
outputHeight = (int)cropBounds.Value.Height;
}
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);
outputWidth = (int)scaledWidth;
outputHeight = (int)scaledHeight;
}
}
path = GetDestinationPath(encoder);
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
{
encoder.Save(outputStream);
path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
{
var winrtOutputStream = outputStream.AsRandomAccessStream();
if (!_settings.RemoveMetadata)
{
// Transcode path: preserves all metadata automatically
winrtInputStream.Seek(0);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(winrtOutputStream, decoder);
if (!noTransformNeeded)
{
encoder.BitmapTransform.ScaledWidth = scaledWidth;
encoder.BitmapTransform.ScaledHeight = scaledHeight;
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
if (cropBounds.HasValue)
{
encoder.BitmapTransform.Bounds = cropBounds.Value;
}
}
await ConfigureEncoderPropertiesAsync(encoder, encoderGuid);
await encoder.FlushAsync();
}
else
{
// Strip metadata path: fresh encoder = no old metadata
var propertySet = GetEncoderPropertySet(encoderGuid);
var encoder = propertySet != null
? await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream, propertySet)
: await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream);
var transform = new BitmapTransform();
if (!noTransformNeeded)
{
transform.ScaledWidth = scaledWidth;
transform.ScaledHeight = scaledHeight;
transform.InterpolationMode = BitmapInterpolationMode.Fant;
if (cropBounds.HasValue)
{
transform.Bounds = cropBounds.Value;
}
}
else
{
transform.ScaledWidth = (uint)originalWidth;
transform.ScaledHeight = (uint)originalHeight;
}
// Handle multi-frame images (e.g., GIF)
for (uint i = 0; i < decoder.FrameCount; i++)
{
if (i > 0)
{
await encoder.GoToNextFrameAsync();
}
var frame = await decoder.GetFrameAsync(i);
var bitmap = await frame.GetSoftwareBitmapAsync(
frame.BitmapPixelFormat,
BitmapAlphaMode.Premultiplied,
transform,
ExifOrientationMode.IgnoreExifOrientation,
ColorManagementMode.DoNotColorManage);
encoder.SetSoftwareBitmap(bitmap);
}
await encoder.FlushAsync();
}
}
}
}
@@ -141,54 +208,74 @@ namespace ImageResizer.Models
}
}
private BitmapEncoder CreateEncoder(Guid containerFormat)
private async Task<string> ExecuteAiAsync(BitmapDecoder decoder, IRandomAccessStream winrtInputStream, Guid encoderGuid)
{
var createdEncoder = BitmapEncoder.Create(containerFormat);
if (!createdEncoder.CanEncode())
try
{
createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
}
// Get the source bitmap for AI processing
var softwareBitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied);
ConfigureEncoder(createdEncoder);
var aiResult = _aiSuperResolutionService.ApplySuperResolution(
softwareBitmap,
_settings.AiSize.Scale,
_file);
return createdEncoder;
void ConfigureEncoder(BitmapEncoder encoder)
{
switch (encoder)
if (aiResult == null)
{
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;
throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
}
var outputWidth = aiResult.PixelWidth;
var outputHeight = aiResult.PixelHeight;
var path = GetDestinationPath(encoderGuid, outputWidth, outputHeight);
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
{
var winrtOutputStream = outputStream.AsRandomAccessStream();
if (!_settings.RemoveMetadata)
{
// Transcode path: preserves metadata
winrtInputStream.Seek(0);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(winrtOutputStream, decoder);
encoder.SetSoftwareBitmap(aiResult);
await ConfigureEncoderPropertiesAsync(encoder, encoderGuid);
await encoder.FlushAsync();
}
else
{
// Strip metadata path
var propertySet = GetEncoderPropertySet(encoderGuid);
var encoder = propertySet != null
? await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream, propertySet)
: await BitmapEncoder.CreateAsync(encoderGuid, winrtOutputStream);
encoder.SetSoftwareBitmap(aiResult);
await encoder.FlushAsync();
}
}
return path;
}
catch (Exception ex) when (ex is not InvalidOperationException)
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
throw new InvalidOperationException(errorMessage, ex);
}
}
private BitmapSource Transform(BitmapSource source)
private (uint ScaledWidth, uint ScaledHeight, BitmapBounds? CropBounds, bool NoTransformNeeded) CalculateDimensions(
int originalWidth, int originalHeight, double dpiX, double dpiY)
{
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);
double width = _settings.SelectedSize.GetPixelWidth(originalWidth, dpiX);
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, dpiY);
// Swap target width/height dimensions if orientation correction is required.
// Ensures that we don't try to fit a landscape image into a portrait box by
// distorting it, unless specific Auto/Percent rules are applied.
bool canSwapDimensions = _settings.IgnoreOrientation &&
!_settings.SelectedSize.HasAuto &&
_settings.SelectedSize.Unit != ResizeUnit.Percent;
@@ -214,15 +301,11 @@ namespace ImageResizer.Models
// Normalize scales based on the chosen Fit/Fill mode.
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
{
// Fit: use the smaller scale to ensure the image fits within the target.
scaleX = Math.Min(scaleX, scaleY);
scaleY = scaleX;
}
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
{
// Fill: use the larger scale to ensure the target area is fully covered.
// This often results in one dimension overflowing, which is handled by
// cropping later.
scaleX = Math.Max(scaleX, scaleY);
scaleY = scaleX;
}
@@ -230,177 +313,107 @@ namespace ImageResizer.Models
// Handle Shrink Only mode.
if (_settings.ShrinkOnly && _settings.SelectedSize.Unit != ResizeUnit.Percent)
{
// Shrink Only mode should never return an image larger than the original.
if (scaleX > 1 || scaleY > 1)
{
return source;
return ((uint)originalWidth, (uint)originalHeight, null, true);
}
// Allow for crop-only when in Fill mode.
// At this point, the scale is <= 1.0. In Fill mode, it is possible for
// the scale to be 1.0 (no resize needed) while the target dimensions are
// smaller than the originals, requiring a crop.
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
(originalWidth > width || originalHeight > height);
// If the scale is exactly 1.0 and a crop isn't required, we return the
// original image to prevent a re-encode.
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
{
return source;
return ((uint)originalWidth, (uint)originalHeight, null, true);
}
}
// Apply the scaling.
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
// Calculate scaled dimensions
uint scaledWidth = (uint)Math.Max(1, (int)Math.Round(originalWidth * scaleX));
uint scaledHeight = (uint)Math.Max(1, (int)Math.Round(originalHeight * scaleY));
// Apply the centered crop for Fill mode, if necessary. Applies when Fill
// mode caused the scaled image to exceed the target dimensions.
// Apply the centered crop for Fill mode, if necessary.
if (_settings.SelectedSize.Fit == ResizeFit.Fill
&& (scaledBitmap.PixelWidth > width
|| scaledBitmap.PixelHeight > height))
&& (scaledWidth > (uint)width || scaledHeight > (uint)height))
{
int x = (int)(((originalWidth * scaleX) - width) / 2);
int y = (int)(((originalHeight * scaleY) - height) / 2);
uint cropX = (uint)(((originalWidth * scaleX) - width) / 2);
uint cropY = (uint)(((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)
var cropBounds = new BitmapBounds
{
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
}
X = cropX,
Y = cropY,
Width = (uint)width,
Height = (uint)height,
};
return result;
return (scaledWidth, scaledHeight, cropBounds, false);
}
catch (Exception ex)
return (scaledWidth, scaledHeight, null, false);
}
private async Task ConfigureEncoderPropertiesAsync(BitmapEncoder encoder, Guid encoderGuid)
{
if (encoderGuid == BitmapEncoder.JpegEncoderId)
{
// Wrap the exception with a localized message
// This will be caught by ResizeBatch.Process() and displayed to the user
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
throw new InvalidOperationException(errorMessage, ex);
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
{
{ "ImageQuality", new BitmapTypedValue((float)MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100) / 100f, PropertyType.Single) },
});
}
}
/// <summary>
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
/// In case of errors, we try to rebuild the metadata object and check again.
/// We return null if we were not able to get hold of valid metadata.
/// </summary>
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
private BitmapPropertySet GetEncoderPropertySet(Guid encoderGuid)
{
if (originalMetadata == null)
if (encoderGuid == BitmapEncoder.JpegEncoderId)
{
return null;
return new BitmapPropertySet
{
{ "ImageQuality", new BitmapTypedValue((float)MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100) / 100f, PropertyType.Single) },
};
}
// Check if the original metadata is valid
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
if (EnsureFrameIsValid(frameWithOriginalMetadata))
if (encoderGuid == BitmapEncoder.TiffEncoderId)
{
return originalMetadata;
var compressionMethod = MapTiffCompression(_settings.TiffCompressOption);
if (compressionMethod.HasValue)
{
return new BitmapPropertySet
{
{ "TiffCompressionMethod", new BitmapTypedValue(compressionMethod.Value, PropertyType.UInt8) },
};
}
}
// Original metadata was invalid. We try to rebuild the metadata object from the scratch and discard invalid metadata fields
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
{
return recreatedMetadata;
}
// Seems like we have an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
return null;
// The safest way to check if the metadata object is valid is to call Save() on the encoder.
// I tried other ways to check if metadata is valid (like calling Clone() on the metadata object) but this was not reliable resulting in a few github issues.
bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
{
try
{
var encoder = CreateEncoder(containerFormat);
encoder.Frames.Add(frameToBeChecked);
using (var testStream = new MemoryStream())
{
encoder.Save(testStream);
}
return true;
}
catch (Exception)
{
return false;
}
}
}
/// <summary>
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
/// </summary>
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
private static byte? MapTiffCompression(TiffCompressOption option)
{
try
return option switch
{
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;
}
TiffCompressOption.None => 1,
TiffCompressOption.Ccitt3 => 2,
TiffCompressOption.Ccitt4 => 3,
TiffCompressOption.Lzw => 4,
TiffCompressOption.Rle => 5,
TiffCompressOption.Zip => 6,
_ => null, // Default: let the encoder decide
};
}
private static BitmapFrame CreateBitmapFrame(BitmapSource transformedBitmap, BitmapMetadata metadata)
{
return BitmapFrame.Create(
transformedBitmap,
thumbnail: null, /* should be null, see #15413 */
metadata,
colorContexts: null /* should be null, see #14866 */ );
}
private string GetDestinationPath(BitmapEncoder encoder)
private string GetDestinationPath(Guid encoderGuid, int outputPixelWidth, int outputPixelHeight)
{
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
var supportedExtensions = encoder.CodecInfo.FileExtensions.Split(',');
var supportedExtensions = CodecHelper.GetSupportedExtensions(encoderGuid);
var extension = _fileSystem.Path.GetExtension(_file);
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
extension = supportedExtensions.FirstOrDefault();
extension = CodecHelper.GetDefaultExtension(encoderGuid);
}
// Remove directory characters from the size's name.
// For AI Size, use the scale display (e.g., "2×") instead of the full name
string sizeName = _settings.SelectedSize is AiSize aiSize
? aiSize.ScaleDisplay
: _settings.SelectedSize.Name;
@@ -408,9 +421,8 @@ namespace ImageResizer.Models
.Replace('\\', '_')
.Replace('/', '_');
// Using CurrentCulture since this is user facing
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
var selectedWidth = _settings.SelectedSize is AiSize ? outputPixelWidth : _settings.SelectedSize.Width;
var selectedHeight = _settings.SelectedSize is AiSize ? outputPixelHeight : _settings.SelectedSize.Height;
var fileName = string.Format(
CultureInfo.CurrentCulture,
_settings.FileNameFormat,
@@ -418,10 +430,9 @@ namespace ImageResizer.Models
sizeNameSanitized,
selectedWidth,
selectedHeight,
encoder.Frames[0].PixelWidth,
encoder.Frames[0].PixelHeight);
outputPixelWidth,
outputPixelHeight);
// Remove invalid characters from the final file name.
fileName = fileName
.Replace(':', '_')
.Replace('*', '_')
@@ -431,7 +442,6 @@ namespace ImageResizer.Models
.Replace('>', '_')
.Replace('|', '_');
// Avoid creating not recommended filenames
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
{
fileName = fileName + "_";

View File

@@ -1,36 +1,54 @@
#pragma warning disable IDE0073
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using ImageResizer.Helpers;
using ImageResizer.Properties;
using ManagedCommon;
namespace ImageResizer.Models
{
public class ResizeSize : Observable, IHasId
public partial class ResizeSize : ObservableObject, IHasId
{
private static readonly Dictionary<string, string> _tokens = new Dictionary<string, string>
{
["$small$"] = Resources.Small,
["$medium$"] = Resources.Medium,
["$large$"] = Resources.Large,
["$phone$"] = Resources.Phone,
};
// Lazy initialization to avoid ResourceLoader call during class loading (enables unit testing)
private static readonly Lazy<Dictionary<string, string>> _tokens = new Lazy<Dictionary<string, string>>(() =>
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;
private bool _showHeight = true;
[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)
@@ -47,73 +65,18 @@ namespace ImageResizer.Models
{
}
[JsonPropertyName("Id")]
public int Id
{
get => _id;
set => Set(ref _id, value);
}
[JsonPropertyName("name")]
public virtual string Name
{
get => _name;
set => Set(ref _name, ReplaceTokens(value));
set => SetProperty(ref _name, ReplaceTokens(value));
}
[JsonPropertyName("fit")]
public ResizeFit Fit
{
get => _fit;
set
{
var previousFit = _fit;
Set(ref _fit, value);
if (!Equals(previousFit, value))
{
UpdateShowHeight();
}
}
}
[JsonPropertyName("width")]
public double Width
{
get => _width;
set => Set(ref _width, value);
}
[JsonPropertyName("height")]
public double Height
{
get => _height;
set => Set(ref _height, value);
}
public bool ShowHeight
{
get => _showHeight;
set => Set(ref _showHeight, value);
}
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
public bool HasAuto
=> Width == 0 || Height == 0 || double.IsNaN(Width) || double.IsNaN(Height);
[JsonPropertyName("unit")]
public ResizeUnit Unit
{
get => _unit;
set
{
var previousUnit = _unit;
Set(ref _unit, value);
if (!Equals(previousUnit, value))
{
UpdateShowHeight();
}
}
}
public double GetPixelWidth(int originalWidth, double dpi)
=> ConvertToPixels(Width, Unit, originalWidth, dpi);
@@ -127,15 +90,10 @@ namespace ImageResizer.Models
dpi);
private static string ReplaceTokens(string text)
=> (text != null && _tokens.TryGetValue(text, out var result))
=> (text != null && _tokens.Value.TryGetValue(text, out var result))
? result
: text;
private void UpdateShowHeight()
{
ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
}
private double ConvertToPixels(double value, ResizeUnit unit, int originalValue, double dpi)
{
if (value == 0 || double.IsNaN(value))

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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.

View 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();
});
}
}
}

View File

@@ -1,9 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ImageResizer.Test")]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ImageResizer.Helpers;
namespace ImageResizer.Properties
{
/// <summary>
/// Resource accessor class for compatibility with CLI code and tests.
/// Wraps ResourceLoader for resource string access.
/// </summary>
internal static class Resources
{
// Size names (used by tests and ResizeSize token replacement)
public static string Small => ResourceLoaderInstance.ResourceLoader.GetString("Small");
public static string Medium => ResourceLoaderInstance.ResourceLoader.GetString("Medium");
public static string Large => ResourceLoaderInstance.ResourceLoader.GetString("Large");
public static string Phone => ResourceLoaderInstance.ResourceLoader.GetString("Phone");
// Input page resources
public static string Input_Custom => ResourceLoaderInstance.ResourceLoader.GetString("Input_Custom");
// Validation messages
public static string ValueMustBeBetween => ResourceLoaderInstance.ResourceLoader.GetString("ValueMustBeBetween");
// CLI options
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");
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/
@@ -17,12 +17,10 @@ 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 ImageResizer.Services;
using ImageResizer.ViewModels;
using ManagedCommon;
using Microsoft.UI.Dispatching;
namespace ImageResizer.Properties
{
@@ -46,7 +44,23 @@ namespace ImageResizer.Properties
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
// Cached UI thread DispatcherQueue for cross-thread property change notifications
private static DispatcherQueue _uiDispatcherQueue;
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();
@@ -74,8 +88,8 @@ namespace ImageResizer.Properties
IgnoreOrientation = true;
RemoveMetadata = false;
JpegQualityLevel = 90;
PngInterlaceOption = System.Windows.Media.Imaging.PngInterlaceOption.Default;
TiffCompressOption = System.Windows.Media.Imaging.TiffCompressOption.Default;
PngInterlaceOption = Models.PngInterlaceOption.Default;
TiffCompressOption = Models.TiffCompressOption.Default;
FileName = "%1 (%2)";
Sizes = new ObservableCollection<ResizeSize>
{
@@ -87,32 +101,25 @@ namespace ImageResizer.Properties
KeepDateModified = false;
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
AiSize = new AiSize(2); // Initialize with default scale of 2
AiSize = new AiSize(2);
AllSizes = new AllSizesCollection(this);
}
/// <summary>
/// Validates the SelectedSizeIndex to ensure it's within the valid range.
/// This handles cross-device migration where settings saved on ARM64 with AI selected
/// are loaded on non-ARM64 devices.
/// </summary>
private void ValidateSelectedSizeIndex()
{
// Index structure: 0 to Sizes.Count-1 (regular), Sizes.Count (CustomSize), Sizes.Count+1 (AiSize)
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
? Sizes.Count // CustomSize only
: Sizes.Count + 1; // CustomSize + AiSize
? Sizes.Count
: Sizes.Count + 1;
if (_selectedSizeIndex > maxIndex)
{
_selectedSizeIndex = 0; // Reset to first size
_selectedSizeIndex = 0;
}
}
[JsonIgnore]
public IEnumerable<ResizeSize> AllSizes { get; set; }
// Using OrdinalIgnoreCase since this is internal and used for comparison with symbols
public string FileNameFormat
=> _fileNameFormat
?? (_fileNameFormat = FileName
@@ -144,7 +151,6 @@ namespace ImageResizer.Properties
}
else
{
// Fallback to CustomSize when index is out of range or AI is not available
return CustomSize;
}
}
@@ -168,13 +174,7 @@ namespace ImageResizer.Properties
}
}
string IDataErrorInfo.Error
{
get
{
return string.Empty;
}
}
string IDataErrorInfo.Error => string.Empty;
string IDataErrorInfo.this[string columnName]
{
@@ -187,7 +187,6 @@ namespace ImageResizer.Properties
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
{
// Using CurrentCulture since this is user facing
return string.Format(CultureInfo.CurrentCulture, ValueMustBeBetween, 1, 100);
}
@@ -217,26 +216,20 @@ namespace ImageResizer.Properties
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;
}
@@ -244,7 +237,6 @@ namespace ImageResizer.Properties
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public int Count
@@ -291,7 +283,6 @@ namespace ImageResizer.Properties
private class AllSizesEnumerator : IEnumerator<ResizeSize>
{
private readonly AllSizesCollection _list;
private int _index = -1;
public AllSizesEnumerator(AllSizesCollection list)
@@ -376,15 +367,6 @@ namespace ImageResizer.Properties
}
}
/// <summary>
/// Gets or sets a value indicating whether resizing images removes any metadata that doesn't affect rendering.
/// Default is false.
/// </summary>
/// <remarks>
/// Preserved Metadata:
/// System.Photo.Orientation,
/// System.Image.ColorSpace
/// </remarks>
[JsonConverter(typeof(WrappedJsonValueConverter))]
[JsonPropertyName("imageresizer_removeMetadata")]
public bool RemoveMetadata
@@ -505,6 +487,15 @@ namespace ImageResizer.Properties
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 = "")
@@ -517,11 +508,9 @@ namespace ImageResizer.Properties
_jsonMutex.WaitOne();
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this }, _jsonSerializerOptions);
// Create directory if it doesn't exist
IFileInfo file = _fileSystem.FileInfo.New(SettingsPath);
file.Directory.Create();
// write string to file
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
_jsonMutex.ReleaseMutex();
}
@@ -554,13 +543,22 @@ namespace ImageResizer.Properties
{
}
if (App.Current?.Dispatcher != null)
// 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)
{
// Needs to be called on the App UI thread as the properties are bound to the UI.
App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings));
// 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);
}
@@ -580,20 +578,16 @@ namespace ImageResizer.Properties
KeepDateModified = jsonSettings.KeepDateModified;
FallbackEncoder = jsonSettings.FallbackEncoder;
CustomSize = jsonSettings.CustomSize;
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
AiSize = jsonSettings.AiSize ?? new AiSize(2);
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
if (jsonSettings.Sizes.Count > 0)
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
// Ensure Ids are unique and handle missing Ids
IdRecoveryHelper.RecoverInvalidIds(Sizes);
}
// Validate SelectedSizeIndex after Sizes collection has been updated
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
ValidateSelectedSizeIndex();
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -3,12 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Media.Imaging;
using Windows.Graphics.Imaging;
namespace ImageResizer.Services
{
public interface IAISuperResolutionService : IDisposable
{
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
}
}

View File

@@ -2,7 +2,7 @@
// 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;
using Windows.Graphics.Imaging;
namespace ImageResizer.Services
{
@@ -14,7 +14,7 @@ namespace ImageResizer.Services
{
}
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
{
return source;
}

View File

@@ -3,13 +3,8 @@
// 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;
@@ -91,7 +86,7 @@ namespace ImageResizer.Services
}
}
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
public SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath)
{
if (source == null || _disposed)
{
@@ -102,19 +97,12 @@ namespace ImageResizer.Services
// 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;
var newWidth = source.PixelWidth * scale;
var newHeight = source.PixelHeight * scale;
// Apply super resolution with thread-safe access
// _usageLock protects concurrent access from Parallel.ForEach threads
// _usageLock protects concurrent access from Parallel.ForEachAsync threads
SoftwareBitmap scaledBitmap;
lock (_usageLock)
{
@@ -123,16 +111,10 @@ namespace ImageResizer.Services
return source;
}
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(source, newWidth, newHeight);
}
if (scaledBitmap == null)
{
return source;
}
// Convert back to WPF BitmapSource
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
return scaledBitmap ?? source;
}
catch (Exception)
{
@@ -141,102 +123,6 @@ namespace ImageResizer.Services
}
}
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)

View File

@@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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">
@@ -117,19 +58,32 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AllFilesFilter" xml:space="preserve">
<value>All Files</value>
<!-- General strings -->
<data name="ImageResizer" xml:space="preserve">
<value>Image Resizer</value>
<comment>Product name, do not loc</comment>
</data>
<data name="Cancel" xml:space="preserve">
<data name="Cancel.Text" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Height" xml:space="preserve">
<value>Height</value>
</data>
<data name="ImageResizer" xml:space="preserve">
<value>Image Resizer</value>
<comment>Product name, do not loc</comment>
<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>
@@ -139,123 +93,26 @@
<data name="Input_Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="Input_IgnoreOrientation" xml:space="preserve">
<value>Ignore the _orientation of pictures</value>
<data name="Input_IgnoreOrientation.Text" xml:space="preserve">
<value>Ignore the orientation of pictures</value>
</data>
<data name="Input_GifWarning" xml:space="preserve">
<data name="Input_GifWarning.Text" xml:space="preserve">
<value>Gif files with animations may not be correctly resized.</value>
</data>
<data name="Input_Replace" xml:space="preserve">
<value>Ov_erwrite files</value>
<data name="Input_Replace.Text" xml:space="preserve">
<value>Overwrite files</value>
</data>
<data name="Input_Resize" xml:space="preserve">
<data name="Input_Resize.Text" xml:space="preserve">
<value>Resize</value>
</data>
<data name="Input_ShrinkOnly" xml:space="preserve">
<value>_Make pictures smaller but not larger</value>
<data name="Input_ShrinkOnly.Text" xml:space="preserve">
<value>Make pictures smaller but not larger</value>
</data>
<data name="Large" xml:space="preserve">
<value>Large</value>
<data name="Input_RemoveMetadata.Text" xml:space="preserve">
<value>Remove metadata that doesn't affect rendering</value>
</data>
<data name="Medium" xml:space="preserve">
<value>Medium</value>
</data>
<data name="OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="OK_Tooltip" xml:space="preserve">
<value>Apply settings</value>
</data>
<data name="Phone" xml:space="preserve">
<value>Phone</value>
</data>
<data name="PictureFilter" xml:space="preserve">
<value>All Picture Files</value>
</data>
<data name="PngInterlaceOption_Default" xml:space="preserve">
<value>(Default)</value>
</data>
<data name="PngInterlaceOption_Off" xml:space="preserve">
<value>Off</value>
</data>
<data name="PngInterlaceOption_On" xml:space="preserve">
<value>On</value>
</data>
<data name="Progress_MainInstruction" xml:space="preserve">
<value>Resizing your pictures...</value>
</data>
<data name="Progress_Stop" xml:space="preserve">
<value>Stop</value>
</data>
<data name="Progress_TimeRemaining_HourMinute" xml:space="preserve">
<value>About {0} hour, {1} minute remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_HourMinutes" xml:space="preserve">
<value>About {0} hour, {1} minutes remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_HoursMinute" xml:space="preserve">
<value>About {0} hours, {1} minute remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_HoursMinutes" xml:space="preserve">
<value>About {0} hours, {1} minutes remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_MinuteSecond" xml:space="preserve">
<value>About {1} minute, {2} second remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_MinuteSeconds" xml:space="preserve">
<value>About {1} minute, {2} seconds remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_MinutesSecond" xml:space="preserve">
<value>About {1} minutes, {2} second remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_MinutesSeconds" xml:space="preserve">
<value>About {1} minutes, {2} seconds remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_Second" xml:space="preserve">
<value>About {2} second remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="Progress_TimeRemaining_Seconds" xml:space="preserve">
<value>About {2} seconds remaining.</value>
<comment>"About" = Approximately, not "on the subject of"</comment>
</data>
<data name="ResizeFit_Fill" xml:space="preserve">
<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>
<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 name="Image_Sizes" xml:space="preserve">
<value>Image sizes</value>
</data>
<data name="Resize_Tooltip" xml:space="preserve">
<value>Resize pictures</value>
@@ -263,42 +120,17 @@
<data name="Resize_Type" xml:space="preserve">
<value>Resize type</value>
</data>
<data name="Results_Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Results_MainInstruction" xml:space="preserve">
<value>Can't resize the following pictures</value>
</data>
<data name="Small" xml:space="preserve">
<value>Small</value>
</data>
<data name="Unit" xml:space="preserve">
<value>Unit</value>
</data>
<data name="ValueMustBeBetween" xml:space="preserve">
<value>Value must be between '{0}' and '{1}'.</value>
</data>
<data name="Version" xml:space="preserve">
<value>Version</value>
</data>
<data name="Width" xml:space="preserve">
<value>Width</value>
</data>
<data name="Open_settings" xml:space="preserve">
<data name="Open_settings.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Input_RemoveMetadata" xml:space="preserve">
<value>Remove meta_data that doesn't affect rendering</value>
</data>
<data name="Image_Sizes" xml:space="preserve">
<value>Image sizes</value>
</data>
<data name="Input_ShrinkOnly.Content" xml:space="preserve">
<value>_Make pictures smaller but not larger</value>
</data>
<!-- 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>
@@ -308,10 +140,10 @@
<data name="Input_AiScaleLabel" xml:space="preserve">
<value>Scale</value>
</data>
<data name="Input_AiCurrentLabel" xml:space="preserve">
<data name="Input_AiCurrentLabel.Text" xml:space="preserve">
<value>Current:</value>
</data>
<data name="Input_AiNewLabel" xml:space="preserve">
<data name="Input_AiNewLabel.Text" xml:space="preserve">
<value>New:</value>
</data>
<data name="Input_AiModelChecking" xml:space="preserve">
@@ -332,7 +164,7 @@
<data name="Input_AiModelDownloadFailed" xml:space="preserve">
<value>Failed to download AI model. Please try again.</value>
</data>
<data name="Input_AiModelDownloadButton" xml:space="preserve">
<data name="Input_AiModelDownloadButton.Text" xml:space="preserve">
<value>Download</value>
</data>
<data name="Error_AiProcessingFailed" xml:space="preserve">
@@ -344,8 +176,84 @@
<data name="Error_AiScalingFailed" xml:space="preserve">
<value>AI scaling operation failed.</value>
</data>
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
<value>Upscale images using on-device AI</value>
<!-- 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 -->
@@ -499,4 +407,7 @@
<data name="CLI_Option_Width" xml:space="preserve">
<value>Set width</value>
</data>
</root>
<data name="ValueMustBeBetween" xml:space="preserve">
<value>Value must be between '{0}' and '{1}'.</value>
</data>
</root>

View File

@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Graphics.Imaging;
namespace ImageResizer.Utilities
{
/// <summary>
/// Maps between legacy container format GUIDs (used in settings JSON) and WinRT encoder/decoder IDs,
/// and provides file extension lookups.
/// </summary>
internal static class CodecHelper
{
// Legacy container format GUID (stored in settings JSON) -> WinRT Encoder ID
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
{
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
};
// WinRT Decoder ID -> WinRT Encoder ID
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
{
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
[BitmapDecoder.BmpDecoderId] = BitmapEncoder.BmpEncoderId,
[BitmapDecoder.TiffDecoderId] = BitmapEncoder.TiffEncoderId,
[BitmapDecoder.GifDecoderId] = BitmapEncoder.GifEncoderId,
[BitmapDecoder.JpegXRDecoderId] = BitmapEncoder.JpegXREncoderId,
};
// Encoder ID -> supported file extensions
private static readonly Dictionary<Guid, string[]> EncoderExtensions = new()
{
[BitmapEncoder.JpegEncoderId] = new[] { ".jpg", ".jpeg", ".jpe", ".jfif" },
[BitmapEncoder.PngEncoderId] = new[] { ".png" },
[BitmapEncoder.BmpEncoderId] = new[] { ".bmp", ".dib", ".rle" },
[BitmapEncoder.TiffEncoderId] = new[] { ".tiff", ".tif" },
[BitmapEncoder.GifEncoderId] = new[] { ".gif" },
[BitmapEncoder.JpegXREncoderId] = new[] { ".jxr", ".wdp" },
};
/// <summary>
/// Gets the WinRT encoder ID that corresponds to the given legacy container format GUID.
/// Falls back to PNG if the GUID is not recognized.
/// </summary>
public static Guid GetEncoderIdFromLegacyGuid(Guid containerFormatGuid)
=> LegacyGuidToEncoderId.TryGetValue(containerFormatGuid, out var id)
? id
: BitmapEncoder.PngEncoderId;
/// <summary>
/// Gets the WinRT encoder ID that matches the given decoder's codec.
/// Returns null if no matching encoder exists (e.g., ICO decoder has no encoder).
/// </summary>
public static Guid? GetEncoderIdForDecoder(BitmapDecoder decoder)
{
var codecId = decoder.DecoderInformation?.CodecId ?? Guid.Empty;
return DecoderIdToEncoderId.TryGetValue(codecId, out var encoderId)
? encoderId
: null;
}
/// <summary>
/// Returns the supported file extensions for the given encoder ID.
/// </summary>
public static string[] GetSupportedExtensions(Guid encoderId)
=> EncoderExtensions.TryGetValue(encoderId, out var extensions)
? extensions
: Array.Empty<string>();
/// <summary>
/// Returns the default (first) file extension for the given encoder ID.
/// </summary>
public static string GetDefaultExtension(Guid encoderId)
=> GetSupportedExtensions(encoderId).FirstOrDefault() ?? ".png";
/// <summary>
/// Checks whether the given encoder ID is a known, supported encoder.
/// </summary>
public static bool CanEncode(Guid encoderId)
=> EncoderExtensions.ContainsKey(encoderId);
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable IDE0073
#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/

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.

View File

@@ -1,89 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using ImageResizer.Helpers;
using ImageResizer.Models;
using ImageResizer.Properties;
namespace ImageResizer.ViewModels
{
public class AdvancedViewModel : Observable
{
private static Dictionary<Guid, string> InitEncoderMap()
{
var bmpCodec = new BmpBitmapEncoder().CodecInfo;
var gifCodec = new GifBitmapEncoder().CodecInfo;
var jpegCodec = new JpegBitmapEncoder().CodecInfo;
var pngCodec = new PngBitmapEncoder().CodecInfo;
var tiffCodec = new TiffBitmapEncoder().CodecInfo;
var wmpCodec = new WmpBitmapEncoder().CodecInfo;
return new Dictionary<Guid, string>
{
[bmpCodec.ContainerFormat] = bmpCodec.FriendlyName,
[gifCodec.ContainerFormat] = gifCodec.FriendlyName,
[jpegCodec.ContainerFormat] = jpegCodec.FriendlyName,
[pngCodec.ContainerFormat] = pngCodec.FriendlyName,
[tiffCodec.ContainerFormat] = tiffCodec.FriendlyName,
[wmpCodec.ContainerFormat] = wmpCodec.FriendlyName,
};
}
public AdvancedViewModel(Settings settings)
{
RemoveSizeCommand = new RelayCommand<ResizeSize>(RemoveSize);
AddSizeCommand = new RelayCommand(AddSize);
Settings = settings;
}
public static IDictionary<Guid, string> EncoderMap { get; } = InitEncoderMap();
public Settings Settings { get; }
public static string Version
=> typeof(AdvancedViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
public static IEnumerable<Guid> Encoders => EncoderMap.Keys;
public ICommand RemoveSizeCommand { get; }
public ICommand AddSizeCommand { get; }
public void RemoveSize(ResizeSize size)
=> Settings.Sizes.Remove(size);
public void AddSize()
=> Settings.Sizes.Add(new ResizeSize());
public void Close(bool accepted)
{
if (accepted)
{
Settings.Save();
return;
}
var selectedSizeIndex = Settings.SelectedSizeIndex;
var shrinkOnly = Settings.ShrinkOnly;
var replace = Settings.Replace;
var ignoreOrientation = Settings.IgnoreOrientation;
Settings.Reload();
Settings.SelectedSizeIndex = selectedSizeIndex;
Settings.ShrinkOnly = shrinkOnly;
Settings.Replace = replace;
Settings.IgnoreOrientation = ignoreOrientation;
}
}
}

View File

@@ -1,13 +0,0 @@
#pragma warning disable IDE0073
// Copyright (c) Brice Lambson
// The Brice Lambson licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
#pragma warning restore IDE0073
namespace ImageResizer.ViewModels
{
public interface ITabViewModel
{
string Header { get; }
}
}

View File

@@ -1,8 +1,6 @@
#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
// 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;
@@ -10,10 +8,10 @@ using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Windows.Graphics.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Common.UI;
using ImageResizer.Helpers;
using ImageResizer.Models;
@@ -23,25 +21,12 @@ using ImageResizer.Views;
namespace ImageResizer.ViewModels
{
public class InputViewModel : Observable
public partial class InputViewModel : ObservableObject
{
public const int DefaultAiScale = 2;
private const int MinAiScale = 1;
private const int MaxAiScale = 8;
private readonly ResizeBatch _batch;
private readonly MainViewModel _mainViewModel;
private readonly IMainView _mainView;
private readonly bool _hasMultipleFiles;
private bool _originalDimensionsLoaded;
private int? _originalWidth;
private int? _originalHeight;
private string _currentResolutionDescription;
private string _newResolutionDescription;
private bool _isDownloadingModel;
private string _modelStatusMessage;
private double _modelDownloadProgress;
public enum Dimension
{
Width,
@@ -55,6 +40,29 @@ namespace ImageResizer.ViewModels
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,
@@ -80,13 +88,6 @@ namespace ImageResizer.ViewModels
settings.PropertyChanged += HandleSettingsPropertyChanged;
}
ResizeCommand = new RelayCommand(Resize, () => CanResize);
CancelCommand = new RelayCommand(Cancel);
OpenSettingsCommand = new RelayCommand(OpenSettings);
EnterKeyPressedCommand = new RelayCommand<KeyPressParams>(HandleEnterKeyPress);
DownloadModelCommand = new RelayCommand(async () => await DownloadModelAsync());
// Initialize AI UI state based on Settings availability
InitializeAiState();
}
@@ -111,94 +112,41 @@ namespace ImageResizer.ViewModels
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
public string CurrentResolutionDescription
{
get => _currentResolutionDescription;
private set => Set(ref _currentResolutionDescription, value);
}
public string NewResolutionDescription
{
get => _newResolutionDescription;
private set => Set(ref _newResolutionDescription, value);
}
// ==================== UI State Properties ====================
// Show AI size descriptions only when AI size is selected and not multiple files
public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
// Helper property: Is model currently being downloaded?
public bool IsModelDownloading => _isDownloadingModel;
public string ModelStatusMessage
{
get => _modelStatusMessage;
private set => Set(ref _modelStatusMessage, value);
}
public double ModelDownloadProgress
{
get => _modelDownloadProgress;
private set => Set(ref _modelDownloadProgress, value);
}
// Show download prompt when: AI size is selected and model is not ready (including downloading)
public bool ShowModelDownloadPrompt =>
Settings?.SelectedSize is AiSize &&
(App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
(App.AiAvailabilityState == AiAvailabilityState.ModelNotReady || IsDownloadingModel);
// Show AI controls when: AI size is selected and AI is ready
public bool ShowAiControls =>
Settings?.SelectedSize is AiSize &&
App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
App.AiAvailabilityState == AiAvailabilityState.Ready;
/// <summary>
/// Gets a value indicating whether the resize operation can proceed.
/// For AI resize: only enabled when AI is fully ready.
/// For non-AI resize: always enabled.
/// </summary>
public bool CanResize
{
get
{
// If AI size is selected, only allow resize when AI is fully ready
if (Settings?.SelectedSize is AiSize)
{
return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
return App.AiAvailabilityState == AiAvailabilityState.Ready;
}
// Non-AI resize can always proceed
return true;
}
}
public ICommand ResizeCommand { get; }
public ICommand CancelCommand { get; }
public ICommand OpenSettingsCommand { get; }
public ICommand EnterKeyPressedCommand { get; private set; }
public ICommand DownloadModelCommand { get; private set; }
// Any of the files is a gif
public bool TryingToResizeGifFiles =>
_batch?.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase)) == true;
_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);
}
public static void OpenSettings()
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
}
private void HandleEnterKeyPress(KeyPressParams parameters)
[RelayCommand]
private void EnterKeyPressed(KeyPressParams parameters)
{
switch (parameters.Dimension)
{
@@ -211,25 +159,79 @@ namespace ImageResizer.ViewModels
}
}
[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):
// Notify UI state properties that depend on SelectedSize
NotifyAiStateChanged();
UpdateAiDetails();
// Trigger CanExecuteChanged for ResizeCommand
if (ResizeCommand is RelayCommand cmd)
{
cmd.OnCanExecuteChanged();
}
ResizeCommand.NotifyCanExecuteChanged();
break;
}
}
@@ -238,16 +240,12 @@ namespace ImageResizer.ViewModels
{
if (Settings?.AiSize != null)
{
Settings.AiSize.Scale = Math.Clamp(
Settings.AiSize.Scale,
MinAiScale,
MaxAiScale);
Settings.AiSize.Scale = Math.Clamp(Settings.AiSize.Scale, MinAiScale, MaxAiScale);
}
}
private void UpdateAiDetails()
private async void UpdateAiDetails()
{
// Clear AI details if AI size not selected
if (Settings == null || Settings.SelectedSize is not AiSize)
{
CurrentResolutionDescription = string.Empty;
@@ -264,25 +262,25 @@ namespace ImageResizer.ViewModels
return;
}
EnsureOriginalDimensionsLoaded();
await EnsureOriginalDimensionsLoadedAsync();
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
CurrentResolutionDescription = hasConcreteSize
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
: Resources.Input_AiUnknownSize;
: ResourceLoaderInstance.ResourceLoader.GetString("Input_AiUnknownSize");
var scale = Settings.AiSize.Scale;
NewResolutionDescription = hasConcreteSize
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
: Resources.Input_AiUnknownSize;
: ResourceLoaderInstance.ResourceLoader.GetString("Input_AiUnknownSize");
}
private static string FormatDimensions(long width, long height)
{
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
return string.Format(CultureInfo.CurrentCulture, "{0} x {1}", width, height);
}
private void EnsureOriginalDimensionsLoaded()
private async Task EnsureOriginalDimensionsLoadedAsync()
{
if (_originalDimensionsLoaded)
{
@@ -298,18 +296,14 @@ namespace ImageResizer.ViewModels
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;
}
using var fileStream = File.OpenRead(file);
using var stream = fileStream.AsRandomAccessStream();
var decoder = await BitmapDecoder.CreateAsync(stream);
_originalWidth = (int)decoder.PixelWidth;
_originalHeight = (int)decoder.PixelHeight;
}
catch (Exception)
{
// Failed to load image dimensions - clear values
_originalWidth = null;
_originalHeight = null;
}
@@ -319,128 +313,43 @@ namespace ImageResizer.ViewModels
}
}
/// <summary>
/// Initializes AI UI state based on App's cached availability state.
/// Subscribe to state change event to update UI when background initialization completes.
/// </summary>
private void InitializeAiState()
{
// Subscribe to initialization completion event to refresh UI
App.AiInitializationCompleted += OnAiInitializationCompleted;
// Set initial status message based on current state
UpdateStatusMessage();
}
/// <summary>
/// Handles AI initialization completion event from App.
/// Refreshes UI when background initialization finishes.
/// </summary>
private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
private void OnAiInitializationCompleted(object sender, AiAvailabilityState finalState)
{
UpdateStatusMessage();
NotifyAiStateChanged();
}
/// <summary>
/// Updates status message based on current App availability state.
/// </summary>
private void UpdateStatusMessage()
{
ModelStatusMessage = App.AiAvailabilityState switch
{
Properties.AiAvailabilityState.Ready => string.Empty,
Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
AiAvailabilityState.Ready => string.Empty,
AiAvailabilityState.ModelNotReady => ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelNotAvailable"),
AiAvailabilityState.NotSupported => ResourceLoaderInstance.ResourceLoader.GetString("Input_AiModelNotSupported"),
_ => string.Empty,
};
}
/// <summary>
/// Notifies UI when AI state changes (model availability, download status).
/// </summary>
private void NotifyAiStateChanged()
{
OnPropertyChanged(nameof(IsModelDownloading));
OnPropertyChanged(nameof(IsDownloadingModel));
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
OnPropertyChanged(nameof(ShowAiControls));
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
OnPropertyChanged(nameof(CanResize));
// Trigger CanExecuteChanged for ResizeCommand
if (ResizeCommand is RelayCommand resizeCommand)
{
resizeCommand.OnCanExecuteChanged();
}
}
/// <summary>
/// Notifies UI when AI scale changes (slider value).
/// </summary>
private void NotifyAiScaleChanged()
{
OnPropertyChanged(nameof(AiSuperResolutionScale));
OnPropertyChanged(nameof(AiScaleDisplay));
UpdateAiDetails();
}
private async Task DownloadModelAsync()
{
try
{
// Set downloading flag and show progress
_isDownloadingModel = true;
ModelStatusMessage = Resources.Input_AiModelDownloading;
ModelDownloadProgress = 0;
NotifyAiStateChanged();
// Create progress reporter to update UI
var progress = new Progress<double>(value =>
{
// progressValue could be 0-1 or 0-100, normalize to 0-100
ModelDownloadProgress = value > 1 ? value : value * 100;
});
// Call EnsureReadyAsync to download and prepare the AI model
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
{
// Model successfully downloaded and ready
ModelDownloadProgress = 100;
// Update App's cached state
App.AiAvailabilityState = Properties.AiAvailabilityState.Ready;
UpdateStatusMessage();
// Initialize the AI service now that model is ready
var aiService = await WinAiSuperResolutionService.CreateAsync();
ResizeBatch.SetAiSuperResolutionService(aiService ?? (Services.IAISuperResolutionService)NoOpAiSuperResolutionService.Instance);
}
else
{
// Download failed
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
}
}
catch (Exception)
{
// Exception during download
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
}
finally
{
// Clear downloading flag
_isDownloadingModel = false;
// Reset progress if not successful
if (App.AiAvailabilityState != Properties.AiAvailabilityState.Ready)
{
ModelDownloadProgress = 0;
}
NotifyAiStateChanged();
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More