Compare commits

...

14 Commits

Author SHA1 Message Date
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
Kai Tao
c5d4f992c1 Workspace: Fix an overlay issue for workspace snapshot draw (#45183)
<!-- 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
Root cause: Workspaces uses DPI-unaware coordinates (via
GetDpiUnawareScreens()
which runs in a temporary DPI-unaware thread) to store/match window
positions
across different DPI settings. However, WorkspacesEditor itself uses
PerMonitorV2
DPI awareness for UI clarity. When assigning these DPI-unaware
coordinates directly
to WPF window properties, WPF automatically scaled them again based on
current DPI,
causing incorrect overlay positioning.

Fix: Use SetWindowPositionDpiUnaware() to bypass WPF's automatic DPI
scaling
by temporarily switching to DPI-unaware context when calling Win32
SetWindowPos.

Fix #45174

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

- [ ] Closes: #45174
<!-- - [ ] 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
Verified in local build vs production build, and the problem fixed in
local build.
2026-02-02 09:34:50 +08:00
Kai Tao
11b406feee Build: Fix release pipeline and local build failure (#45211)
## Summary of the Pull Request
Release pipeline is keeping failed, and local build failed at ut.

This pull request introduces changes to improve how test projects are
handled during release builds, ensuring that test code is not compiled
or analyzed when not needed - in doing release build, to - succeed the
execution and reduce built time.

And, to upgrade from VS17 to VS18 in cmdpal sdk build, this is to keep
consistency with all other build step


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

- [ ] Closes: #xxx
<!-- - [ ] 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
Local build & release pipeline build should all pass:

Local build:
<img width="1815" height="281" alt="image"
src="https://github.com/user-attachments/assets/f350cf3f-b856-432d-97f3-e392d38ef7fa"
/>

Release pipeline is working too:
<img width="1195" height="163" alt="image"
src="https://github.com/user-attachments/assets/ce58de38-f0fb-45ad-9d70-2b8eb1c4db60"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-02 09:15:53 +08:00
Jiří Polášek
256af8f6e0 Spellcheck: Add missing words and sort expect.txt (#45251)
## PR Checklist

This PR adds missing words to the spell checker dictionary.

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-02-01 20:14:37 +08:00
Gordon Lam
87c65f9eec docs(paste): add AI preview credit documentation (#45236)
docs(paste): add AI preview credit documentation

```markdown
## Summary of the Pull Request

Adds documentation clarifying that the "Show preview" setting for Paste with AI does not consume additional AI credits. The preview displays the same AI response that was already generated from a single API call, cached locally.

## PR Checklist

- [x] Closes: #32950
- [x] **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 - N/A (documentation only)
- [ ] **Localization:** All end-user-facing strings can be localized - N/A (dev docs only)
- [x] **Dev docs:** Added/updated
- [ ] **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

## Detailed Description of the Pull Request / Additional comments

This PR addresses the question raised in issue #32950 about whether enabling preview for Paste with AI costs extra AI quota.

Changes to `doc/devdocs/modules/advancedpaste.md`:
- Added new "Paste with AI Preview" section explaining:
  - The `ShowCustomPreview` setting behavior
  - Confirmation that preview does **not** consume additional AI credits
  - The implementation flow showing a single API call with local caching
  - Reference to `OptionsViewModel.cs` lines 702-717
- Added settings documentation table for `ShowCustomPreview`

Fixes #32950

## Validation Steps Performed

- Verified documentation renders correctly in Markdown preview
- Confirmed technical accuracy by referencing `OptionsViewModel.cs` implementation
```

---------

Co-authored-by: yeelam-gordon <yeelam-gordon@users.noreply.github.com>
2026-01-31 09:03:24 -08:00
Gordon Lam
971c7e9fba docs(settings-ui): update Advanced Paste OOBE description for AI features (#45233)
## Summary of the Pull Request

Updates the Advanced Paste OOBE (Out-of-Box Experience) description to
accurately reflect that AI features no longer require specifically an
OpenAI API key. The new text clarifies:
- Changed "markdown" to "Markdown" and "json" to "JSON" for proper
casing
- Replaced "100% opt-in and requires an Open AI key" with "opt-in AI
feature that can use an online or local language model endpoint"

This fixes the outdated description that still referenced OpenAI as the
only option.

## PR Checklist

- [x] Closes: #44044
- [x] **Communication:** Documentation/string fix, no core contributor
discussion needed
- [ ] **Tests:** N/A - string-only change
- [x] **Localization:** The updated string is in the localizable
Resources.resw file
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** N/A
- [ ] **Documentation updated:** N/A

## Detailed Description of the Pull Request / Additional comments

The change updates
\src/settings-ui/Settings.UI/Strings/en-us/Resources.resw\ to fix the
\Oobe_AdvancedPaste.Description\ string that incorrectly stated AI
features require an OpenAI key.

## Validation Steps Performed

- Verified the string change is valid XML
- Confirmed the updated description accurately reflects current Advanced
Paste AI capabilities
2026-01-31 08:46:31 -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
124 changed files with 2649 additions and 3740 deletions

View File

@@ -635,6 +635,7 @@ GMEM
GNumber
googleai
googlegemini
Gotchas
gpedit
gpo
GPOCA
@@ -647,8 +648,6 @@ GSM
gtm
guiddata
GUITHREADINFO
Gotcha
Gotchas
GValue
gwl
GWLP
@@ -894,9 +893,9 @@ Lclean
Ldone
Ldr
LEFTALIGN
leftclick
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -1022,9 +1021,12 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
Metacharacter
metafile
Metacharacter
mfc
Mgmt
Microwaved
@@ -1071,7 +1073,7 @@ mouseutils
MOVESIZEEND
MOVESIZESTART
MRM
MRT
Mrt
mru
MSAL
msc
@@ -1489,7 +1491,9 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
registryroot
regkey
regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1534,7 +1538,6 @@ riid
RKey
RNumber
rollups
ROOTOWNER
rop
ROUNDSMALL
ROWSETEXT
@@ -2171,4 +2174,4 @@ Zoneszonabletester
Zoomin
zoomit
ZOOMITX
Zorder
Zorder

View File

@@ -134,8 +134,8 @@
"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",

View File

@@ -91,6 +91,7 @@ extends:
official: true
codeSign: true
runTests: false
buildTests: false
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)

View File

@@ -258,6 +258,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildTests=${{ parameters.buildTests }}
/bl:$(LogOutputDirectory)\build-0-main.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)

View File

@@ -59,6 +59,7 @@ stages:
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
@@ -78,7 +79,9 @@ stages:
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
demands: ImageOverride -equals SHINE-VS18-Preview
${{ else }}:
demands: ImageOverride -equals SHINE-VS18-Latest
buildConfigurations: [Release]
official: false
codeSign: false

View File

@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
$currentNoticePackageList = ""
}
# Test-only packages that are allowed to be in NOTICE.md but not in the build
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
$allowedExtraPackages = @(
"- Moq"
)
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
{
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
# Show detailed differences
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in proj file list but not in NOTICE.md
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
if ($missingFromNotice.Count -gt 0) {
Write-Host -ForegroundColor Red "MissingFromNotice:"
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
foreach ($pkg in $missingFromNotice) {
Write-Host -ForegroundColor Red " $pkg"
}
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in NOTICE.md but not in proj file list
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
if ($extraInNotice.Count -gt 0) {
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
foreach ($pkg in $extraInNotice) {
Write-Host -ForegroundColor Yellow " $pkg"
# Filter out allowed extra packages (test-only dependencies)
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
if ($allowedExtra.Count -gt 0) {
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
foreach ($pkg in $allowedExtra) {
Write-Host -ForegroundColor Green " $pkg"
}
Write-Host ""
}
if ($unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
foreach ($pkg in $unexpectedExtra) {
Write-Host -ForegroundColor Red " $pkg"
}
Write-Host ""
}
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
Write-Host " Proj file list has $($generatedPackages.Count) packages"
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
Write-Host ""
exit 1
# Fail if there are missing packages OR unexpected extra packages
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
exit 1
} else {
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
}
}
exit 0

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

@@ -2,6 +2,12 @@
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Skip building C++ test projects when BuildTests=false -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
<RunCodeAnalysis>false</RunCodeAnalysis>
</PropertyGroup>
<!-- Project configurations -->
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">

View File

@@ -19,6 +19,39 @@
<PlatformTarget>$(Platform)</PlatformTarget>
</PropertyGroup>
<!--
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
-->
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
<!-- Match projects starting with UnitTests- or UITest- prefix -->
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
<!-- Match projects in a Tests folder -->
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
</PropertyGroup>
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<EnableDefaultItems>false</EnableDefaultItems>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateGlobalUsings>false</GenerateGlobalUsings>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Disable all code analysis for skipped test projects -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<RunAnalyzers>false</RunAnalyzers>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<Version>$(Version).0</Version>
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
@@ -30,7 +63,9 @@
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -28,4 +28,41 @@
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
</PropertyGroup>
</Project>
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<BuildDependsOn />
<CoreBuildDependsOn />
<RebuildDependsOn />
</PropertyGroup>
<!-- For C# projects: remove all items -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
<PackageReference Remove="@(PackageReference)" />
<ProjectReference Remove="@(ProjectReference)" />
<Reference Remove="@(Reference)" />
<Compile Remove="@(Compile)" />
<Content Remove="@(Content)" />
<EmbeddedResource Remove="@(EmbeddedResource)" />
<None Remove="@(None)" />
<Using Remove="@(Using)" />
<GlobalUsing Remove="@(GlobalUsing)" />
</ItemGroup>
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
<ClCompile Remove="@(ClCompile)" />
<ClInclude Remove="@(ClInclude)" />
<Link Remove="@(Link)" />
<Lib Remove="@(Lib)" />
<ProjectReference Remove="@(ProjectReference)" />
<None Remove="@(None)" />
<ResourceCompile Remove="@(ResourceCompile)" />
<Midl Remove="@(Midl)" />
</ItemGroup>
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
on the Target element still override the default targets even when condition is false. -->
</Project>

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

@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
TODO: Add implementation details
### Paste with AI Preview
The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
The implementation flow:
1. User initiates "Paste with AI" action
2. A single AI API call is made via `ExecutePasteFormatAsync`
3. The result is cached in `GeneratedResponses`
4. If preview is enabled, the cached result is displayed in the preview UI
5. User can paste the cached result without any additional API calls
See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
## Debugging
TODO: Add debugging information
## Settings
TODO: Add settings documentation
| Setting | Description |
|---------|-------------|
| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
## Future Improvements

View File

@@ -367,6 +367,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?>

View File

@@ -2,8 +2,11 @@
// 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;
using WorkspacesEditor.Utils;
namespace WorkspacesEditor
{
/// <summary>
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
/// </summary>
public partial class OverlayWindow : Window
{
private int _targetX;
private int _targetY;
private int _targetWidth;
private int _targetHeight;
public OverlayWindow()
{
InitializeComponent();
SourceInitialized += OnWindowSourceInitialized;
}
/// <summary>
/// Sets the target bounds for the overlay window.
/// The window will be positioned using DPI-unaware context after initialization.
/// </summary>
public void SetTargetBounds(int x, int y, int width, int height)
{
_targetX = x;
_targetY = y;
_targetWidth = width;
_targetHeight = height;
// Set initial WPF properties (will be corrected after HWND creation)
Left = x;
Top = y;
Width = width;
Height = height;
}
private void OnWindowSourceInitialized(object sender, EventArgs e)
{
// Reposition window using DPI-unaware context to match the virtual coordinates.
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
}
}
}

View File

@@ -4,6 +4,8 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace WorkspacesEditor.Utils
{
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
/// <summary>
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
/// </summary>
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
{
var helper = new WindowInteropHelper(window).Handle;
if (helper != IntPtr.Zero)
{
// Temporarily switch to DPI-unaware context to position window.
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
try
{
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
}
finally
{
SetThreadDpiAwarenessContext(oldContext);
}
}
}
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);

View File

@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
{
var bounds = screen.Bounds;
OverlayWindow overlayWindow = new OverlayWindow();
overlayWindow.Top = bounds.Top;
overlayWindow.Left = bounds.Left;
overlayWindow.Width = bounds.Width;
overlayWindow.Height = bounds.Height;
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
overlayWindow.ShowActivated = true;
overlayWindow.Topmost = true;
overlayWindow.Show();

View File

@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />

View File

@@ -84,7 +84,13 @@
<WarningsNotAsErrors>IL2081;$(WarningsNotAsErrors)</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<!-- InternalsVisibleTo with public key for CI builds (signed assemblies) -->
<ItemGroup Condition="'$(CIBuild)'=='true'">
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests, PublicKey=002400000c80000014010000060200000024000052534131000800000100010085aad0bef0688d1b994a0d78e1fd29fc24ac34ed3d3ac3fb9b3d0c48386ba834aa880035060a8848b2d8adf58e670ed20914be3681a891c9c8c01eef2ab22872547c39be00af0e6c72485d7cfd1a51df8947d36ceba9989106b58abe79e6a3e71a01ed6bdc867012883e0b1a4d35b1b5eeed6df21e401bb0c22f2246ccb69979dc9e61eef262832ed0f2064853725a75485fa8a3efb7e027319c86dec03dc3b1bca2b5081bab52a627b9917450dfad534799e1c7af58683bdfa135f1518ff1ea60e90d7b993a6c87fd3dd93408e35d1296f9a7f9a97c5db56c0f3cc25ad11e9777f94d138b3cea53b9a8331c2e6dcb8d2ea94e18bf1163ff112a22dbd92d429a" />
</ItemGroup>
<!-- InternalsVisibleTo without public key for local builds (unsigned assemblies) -->
<ItemGroup Condition="'$(CIBuild)'!='true'">
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests" />
</ItemGroup>
</Project>

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>
<!--<PreBuildEvent>
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
</PreBuildEvent>
</PreBuildEvent>-->
</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>
<!--<PreBuildEvent>
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
</PreBuildEvent>
</PreBuildEvent>-->
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="framework.h" />

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,11 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ImageResizer</RootNamespace>
<AssemblyName>ImageResizer.Test</AssemblyName>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\</OutputPath>
<!-- Enable WPF for System.Windows.Media.Imaging support in tests -->
<UseWPF>true</UseWPF>
</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

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

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

@@ -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,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,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:local="using:ImageResizer"
xmlns:converters="using:ImageResizer.Converters"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default" />
<ResourceDictionary x:Key="Light" />
<ResourceDictionary x:Key="Dark" />
</ResourceDictionary.ThemeDictionaries>
<!-- Converters -->
<converters:AutoDoubleConverter x:Key="AutoDoubleConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:EnumToIntConverter x:Key="EnumToIntConverter" />
<converters:EnumValueConverter x:Key="EnumValueConverter" />
<converters:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
<converters:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
<converters:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
<converters:TimeRemainingConverter x:Key="TimeRemainingConverter" />
<tkconverters:BoolToVisibilityConverter
x:Key="InvertedBoolToVisibilityConverter"
TrueValue="Collapsed"
FalseValue="Visible" />
</ResourceDictionary>
</Application.Resources>
</Application>

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,340 @@
<!-- 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}}">
<!-- ProgressRing commented out for WPF compatibility -->
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
Text="{Binding ModelStatusMessage, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<!-- AI Scale Controls -->
<StackPanel Visibility="{Binding ShowAiControls, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<TextBlock x:Uid="Input_AiCurrentLabel" />
<TextBlock HorizontalAlignment="Right" Text="{Binding AiScaleDisplay, Mode=OneWay}" />
</Grid>
<Slider
Margin="0,8,0,0"
Maximum="8"
Minimum="1"
TickFrequency="1"
TickPlacement="BottomRight"
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
<StackPanel
Margin="0,16,0,0"
Visibility="{Binding ShowAiSizeDescriptions, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<TextBlock x:Uid="Input_AiCurrentLabel" Foreground="{StaticResource TextFillColorSecondaryBrush}" />
<TextBlock
HorizontalAlignment="Right"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Text="{Binding CurrentResolutionDescription, Mode=OneWay}" />
</Grid>
<Grid Margin="0,8,0,0">
<TextBlock x:Uid="Input_AiNewLabel" />
<TextBlock HorizontalAlignment="Right" Text="{Binding NewResolutionDescription, Mode=OneWay}" />
</Grid>
</StackPanel>
</StackPanel>
</Grid>
<!-- Custom input matrix -->
<Grid
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"
Value="{Binding Settings.SelectedSize.Height, Mode=TwoWay}"
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<FontIcon
Grid.Row="2"
VerticalAlignment="Center"
FontSize="20"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Glyph="&#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

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

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,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,13 +14,12 @@ using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ImageResizer.Extensions;
using ImageResizer.Helpers;
using ImageResizer.Properties;
using ImageResizer.Services;
using ImageResizer.Utilities;
using Microsoft.VisualBasic.FileIO;
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
namespace ImageResizer.Models
@@ -35,7 +34,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 =
@@ -187,8 +199,6 @@ namespace ImageResizer.Models
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.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 +224,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,21 +236,14 @@ 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;
}
// 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;
@@ -254,8 +253,7 @@ namespace ImageResizer.Models
// Apply the scaling.
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, 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))
@@ -280,25 +278,18 @@ namespace ImageResizer.Models
if (result == null)
{
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
}
return result;
}
catch (Exception ex)
{
// Wrap the exception with a localized message
// This will be caught by ResizeBatch.Process() and displayed to the user
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
throw new InvalidOperationException(errorMessage, ex);
}
}
/// <summary>
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
/// In case of errors, we try to rebuild the metadata object and check again.
/// We return null if we were not able to get hold of valid metadata.
/// </summary>
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
{
if (originalMetadata == null)
@@ -306,14 +297,12 @@ namespace ImageResizer.Models
return null;
}
// Check if the original metadata is valid
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
if (EnsureFrameIsValid(frameWithOriginalMetadata))
{
return originalMetadata;
}
// 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))
@@ -321,11 +310,8 @@ namespace ImageResizer.Models
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
@@ -346,9 +332,6 @@ namespace ImageResizer.Models
}
}
/// <summary>
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
/// </summary>
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
{
try
@@ -382,9 +365,9 @@ namespace ImageResizer.Models
{
return BitmapFrame.Create(
transformedBitmap,
thumbnail: null, /* should be null, see #15413 */
thumbnail: null,
metadata,
colorContexts: null /* should be null, see #14866 */ );
colorContexts: null);
}
private string GetDestinationPath(BitmapEncoder encoder)
@@ -399,8 +382,6 @@ namespace ImageResizer.Models
extension = supportedExtensions.FirstOrDefault();
}
// 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,7 +389,6 @@ 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 fileName = string.Format(
@@ -421,7 +401,6 @@ namespace ImageResizer.Models
encoder.Frames[0].PixelWidth,
encoder.Frames[0].PixelHeight);
// Remove invalid characters from the final file name.
fileName = fileName
.Replace(':', '_')
.Replace('*', '_')
@@ -431,7 +410,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/
@@ -18,11 +18,10 @@ 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 +45,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();
@@ -87,32 +102,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 +152,6 @@ namespace ImageResizer.Properties
}
else
{
// Fallback to CustomSize when index is out of range or AI is not available
return CustomSize;
}
}
@@ -168,13 +175,7 @@ namespace ImageResizer.Properties
}
}
string IDataErrorInfo.Error
{
get
{
return string.Empty;
}
}
string IDataErrorInfo.Error => string.Empty;
string IDataErrorInfo.this[string columnName]
{
@@ -187,7 +188,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 +217,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 +238,6 @@ namespace ImageResizer.Properties
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public int Count
@@ -291,7 +284,6 @@ namespace ImageResizer.Properties
private class AllSizesEnumerator : IEnumerator<ResizeSize>
{
private readonly AllSizesCollection _list;
private int _index = -1;
public AllSizesEnumerator(AllSizesCollection list)
@@ -376,15 +368,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 +488,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 +509,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 +544,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 +579,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

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

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

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