Compare commits

...

7 Commits

Author SHA1 Message Date
Yu Leng
5d0fe4bbea init 2025-11-05 13:59:35 +08:00
Kai Tao
c364aa7c70 Cmdpal: Bring vcruntime in cmdpal (#43255)
<!-- 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
Cmdpal has dependency on vcruntime appx package due to adaptive card,
bring that dependency in

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

- [x] Closes: #42497
- [ ] **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="1873" height="1183" alt="image"
src="https://github.com/user-attachments/assets/d416d09f-f4d6-456c-a625-ca724a69b5ee"
/>

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-11-05 09:51:25 +08:00
leileizhang
229bedd09f [UI Tests] Add complete OCR UI test coverage (#41947)
<!-- 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
* Enable Text Extractor. Then:
   - [x] Press the activation shortcut and verify the overlay appears.
   - [x] Press Escape and verify the overlay disappears.
   - [x] Press the activation shortcut and verify the overlay appears.
   - [x] Right-click and select Cancel. Verify the overlay disappears.
- [x] Disable Text Extractor and verify that the activation shortuct no
longer activates the utility.
 * With Text Extractor enabled and activated:
   - [x] Try to select text and verify it is copied to the clipboard.
- [x] Try to select a different OCR language by right-clicking and
verify the change is applied.
 * Test the different settings and verify they are applied:
   - [x] Activation shortcut
   - [x] OCR Language
 
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **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
2025-11-04 11:59:10 +08:00
moooyo
70e1177a6a [PowerRename] Support using photo metadata to replace in the PowerRename (#41728)
<!-- 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
1. Introduce WIC for power rename and add new class WICMetadataExtractor
to use WIC to extract metadata.
2. Add some patterns for metadata extract.
3. Support XMP and EXIF metadata extract.
4. Add test data for xmp and exif extractor
5. Add attribution for the test data uploader.

UI:
<img width="2052" height="1415" alt="image"
src="https://github.com/user-attachments/assets/9051b12e-4e66-4fdc-a4d4-3bada661c235"
/>
<img width="284" height="170" alt="image"
src="https://github.com/user-attachments/assets/2fd67193-77a7-48f0-a5ac-08a69fe64e55"
/>
<img width="715" height="1160" alt="image"
src="https://github.com/user-attachments/assets/5fa68a8c-d129-44dd-b747-099dfbcded12"
/>

demo:


https://github.com/user-attachments/assets/e90bc206-62e5-4101-ada2-3187ee7e2039



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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2025-11-04 09:27:16 +08:00
Jiří Polášek
957b653210 CmdPal: Add a micro global error handler (#41392)
## Summary of the Pull Request

This PR introduces a scaled-down version of the global error handler
from #41061.

- Catches and logs virtually all unhandled exceptions.
- For UI thread exceptions, generates an additional error report:
    - One copy is saved in app's log folder for the Bug Report Tool.
- Another copy can be placed to the user’s desktop to increase
visibility and encourage report submission (disabled for now).
- Displays a message box that tells the user where to find the saved
report.

This PR is intentionally minimal and focused. The complete, more
polished solution is still planned in #41061, which should replace this
implementation in a follow-up.

<details><summary>Report example</summary>
<pre>

This is an error report generated by Windows Command Palette.
If you are seeing this message, it means the application has encountered
an unexpected issue.
You can help us fix it by filing a report at
https://aka.ms/powerToysReportBug.
============================================================
😢 An unexpected error occurred in the application.

Summary:
  Message:    NamedResource Not Found.

NamedResource Not Found.

  Type:       System.Runtime.InteropServices.COMException
  Source:     WinRT.Runtime
  Time:       2025-08-26 20:22:53.5752505
  HRESULT:    0x80073B17 (-2147009769)

Stack Trace:
at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
at
ABI.Microsoft.Windows.ApplicationModel.Resources.IResourceLoaderMethods.GetString(IObjectReference
_obj, String resourceId)
at
Microsoft.Windows.ApplicationModel.Resources.ResourceLoader.GetString(String
resourceId)
at Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.GetString(String
resourceId)
at
Microsoft.CmdPal.UI.Settings.SettingsWindow.AnnounceNavigationPaneStateChanged(DependencyObject
sender, DependencyProperty dp)
at
ABI.Microsoft.UI.Xaml.DependencyPropertyChangedCallback.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr dp)

------------------ Full Exception Details ------------------
System.Runtime.InteropServices.COMException (0x80073B17): NamedResource
Not Found.

NamedResource Not Found.

at WinRT.ExceptionHelpers.<ThrowExceptionForHR>g__Throw|38_0(Int32 hr)
at
ABI.Microsoft.Windows.ApplicationModel.Resources.IResourceLoaderMethods.GetString(IObjectReference
_obj, String resourceId)
at
Microsoft.Windows.ApplicationModel.Resources.ResourceLoader.GetString(String
resourceId)
at Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.GetString(String
resourceId)
at
Microsoft.CmdPal.UI.Settings.SettingsWindow.AnnounceNavigationPaneStateChanged(DependencyObject
sender, DependencyProperty dp)
at
ABI.Microsoft.UI.Xaml.DependencyPropertyChangedCallback.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr dp)

ℹ️ If you need further assistance, please include this information in
your support request.
ℹ️ Before sending, take a quick look to make sure it doesn't contain any
personal or sensitive information.
============================================================


</pre>
</details> 

Message:
<img width="2309" height="1341" alt="image"
src="https://github.com/user-attachments/assets/3ec20054-402f-421d-bebd-bf848a8eb504"
/>

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

- [x] Partially handles: #41606
- [ ] **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
2025-10-31 12:35:37 -05:00
Dustin L. Howett
0b0ad68b60 build: build the Machine and User installers at the same time (#42888) 2025-10-31 00:28:12 -05:00
Dustin L. Howett
b87be7263d Tests: do not use *relative drive letter* (#43135)
Since we switched to running the build on C:\, this test started
failing. C: means "current directory on the C drive"!
2025-10-31 11:33:44 +08:00
71 changed files with 5652 additions and 380 deletions

View File

@@ -321,3 +321,10 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
FFF
HHH
riday
YYY

View File

@@ -97,6 +97,7 @@ atl
ATX
ATRIOX
aumid
authenticode
Authenticode
AUTOBUDDY
AUTOCHECKBOX
@@ -142,6 +143,7 @@ bmi
BNumber
BODGY
BOklab
Bootstrappers
BOOTSTRAPPERINSTALLFOLDER
BOTTOMALIGN
boxmodel
@@ -169,9 +171,12 @@ BYPOSITION
CALCRECT
CALG
callbackptr
cabstr
calpwstr
caub
Cangjie
CANRENAME
Carlseibert
Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
@@ -277,6 +282,7 @@ cpptools
cppvsdbg
cppwinrt
createdump
creativecommons
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
@@ -339,6 +345,7 @@ Deact
debugbreak
decryptor
Dedup
dfx
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
@@ -512,6 +519,7 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FNumber
FARPROC
fdx
fesf
@@ -695,6 +703,7 @@ HTCLIENT
hthumbnail
HTOUCHINPUT
HTTRANSPARENT
hutchinsoniana
HVal
HValue
Hvci
@@ -716,7 +725,9 @@ IDCANCEL
IDD
idk
idl
IIM
idlist
ifd
IDOK
IDOn
IDR
@@ -733,6 +744,7 @@ Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IPTC
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
@@ -872,6 +884,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
lon
LOGMSG
LOGPIXELSX
LOGPIXELSY
@@ -963,6 +976,7 @@ MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metadatas
metadatamatters
metafile
mfc
Mgmt
@@ -1030,6 +1044,7 @@ msiexec
MSIFASTINSTALL
MSIHANDLE
MSIRESTARTMANAGERCONTROL
MSIs
msixbundle
MSIXCA
MSLLHOOKSTRUCT
@@ -1121,6 +1136,7 @@ NONCLIENTMETRICSW
NONELEVATED
nonspace
nonstd
nullrefs
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
@@ -1277,6 +1293,7 @@ pnid
PNMLINK
Poc
Podcasts
Photoshop
POINTERID
POINTERUPDATE
Pokedex
@@ -1479,6 +1496,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
samsung
sancov
SAVEFAILED
scanled
@@ -1841,6 +1859,7 @@ USEINSTALLERFORTEST
USESHOWWINDOW
USESTDHANDLES
USRDLL
utm
UType
uuidv
uwp
@@ -1930,6 +1949,7 @@ wgpocpl
WHEREID
wic
wifi
wikimedia
wikipedia
WIL
winapi
@@ -2024,7 +2044,9 @@ XAxis
XButton
xclip
xcopy
xap
XDeployment
XDimension
xdf
XDocument
XElement
@@ -2042,6 +2064,7 @@ xsi
XSpeed
XStr
xstyler
xmp
XTimer
XUP
XVIRTUALSCREEN
@@ -2049,6 +2072,7 @@ xxxxxx
YAxis
ycombinator
YIncrement
YDimension
yinle
yinyue
YPels

View File

@@ -1,53 +0,0 @@
{
"Version": "1.0.0",
"UseMinimatch": false,
"SignBatches": [
{
"MatchedPath": [
"PowerToysSetupCustomActionsVNext.dll",
"SilentFilesInUseBAFunction.dll",
"PowerToys*Setup-*.exe",
"PowerToys*Setup-*.msi"
],
"SigningInfo": {
"Operations": [
{
"KeyCode": "CP-230012",
"OperationSetCode": "SigntoolSign",
"Parameters": [
{
"parameterName": "OpusName",
"parameterValue": "Microsoft"
},
{
"parameterName": "OpusInfo",
"parameterValue": "http://www.microsoft.com"
},
{
"parameterName": "FileDigest",
"parameterValue": "/fd \"SHA256\""
},
{
"parameterName": "PageHash",
"parameterValue": "/NPH"
},
{
"parameterName": "TimeStamp",
"parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
}
],
"ToolName": "sign",
"ToolVersion": "1.0"
},
{
"KeyCode": "CP-230012",
"OperationSetCode": "SigntoolVerify",
"Parameters": [],
"ToolName": "sign",
"ToolVersion": "1.0"
}
]
}
}
]
}

View File

@@ -512,14 +512,6 @@ jobs:
versionNumber: ${{ parameters.versionNumber }}
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
- template: steps-build-installer-vnext.yml
parameters:
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
versionNumber: ${{ parameters.versionNumber }}
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
buildUserInstaller: true # NOTE: This is the distinction between the above and below rules
# This saves ~1GiB per architecture. We won't need these later.
# Removes:
# - All .pdb files from any static libs .libs (which were only used during linking)

View File

@@ -2,9 +2,6 @@ parameters:
- name: versionNumber
type: string
default: "0.0.1"
- name: buildUserInstaller
type: boolean
default: false
- name: codeSign
type: boolean
default: false
@@ -25,43 +22,26 @@ steps:
arguments: 'install --global wix --version 5.0.2'
- pwsh: |-
& git clean -xfd -e *exe -- .\installer\
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Clean installer to reduce cross-contamination
- pwsh: |-
# Determine whether this is a per-user build
$IsPerUser = $${{ parameters.buildUserInstaller }}
# Build slug used to locate the artifacts
$InstallerBuildSlug = if ($IsPerUser) { 'UserSetup' } else { 'MachineSetup' }
# VNext bundle folder; base name intentionally omits the VNext suffix
$InstallerFolder = 'PowerToysSetupVNext'
if ($IsPerUser) {
$InstallerBasename = "PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
}
else {
$InstallerBasename = "PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
}
# Export variables for downstream steps
Write-Host "##vso[task.setvariable variable=InstallerBuildSlug]$InstallerBuildSlug"
Write-Host "##vso[task.setvariable variable=InstallerRelativePath]$(BuildPlatform)\$(BuildConfiguration)\$InstallerBuildSlug"
Write-Host "##vso[task.setvariable variable=InstallerBasename]$InstallerBasename"
Write-Host "##vso[task.setvariable variable=InstallerFolder]$InstallerFolder"
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Prepare Installer variables
Write-Host "##vso[task.setvariable variable=InstallerMachineRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\MachineSetup"
Write-Host "##vso[task.setvariable variable=InstallerUserRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\UserSetup"
Write-Host "##vso[task.setvariable variable=InstallerMachineBasename]PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
Write-Host "##vso[task.setvariable variable=InstallerUserBasename]PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
displayName: Prepare Installer variables
# This dll needs to be built and signed before building the MSI.
# The Custom Actions project contains a pre-build event that prepares the .wxs files
# by filling them out with all our components. We pass RunBuildEvents=true to force
# that logic to run.
- task: VSBuild@1
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build PowerToysSetupCustomActionsVNext
displayName: Build Shared Support DLLs
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysSetupCustomActionsVNext
/p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
/t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction
/p:RunBuildEvents=true;RestorePackagesConfig=true;CIBuild=true
-restore -graph
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog
/bl:$(LogOutputDirectory)\installer-actions.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -70,28 +50,53 @@ steps:
maximumCpuCount: true
- ${{ if eq(parameters.codeSign, true) }}:
- template: steps-esrp-signing.yml
- template: steps-esrp-sign-files-authenticode.yml
parameters:
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign PowerToysSetupCustomActionsVNext
displayName: Sign Shared Support DLLs
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: 'installer/PowerToysSetupCustomActionsVNext/$(InstallerRelativePath)'
signType: batchSigning
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
folder: 'installer'
pattern: |-
**/PowerToysSetupCustomActionsVNext.dll
**/SilentFilesInUseBAFunction.dll
## INSTALLER START
#### MSI BUILDING AND SIGNING
#
# The MSI build contains code that reverts the .wxs files to their in-tree versions.
# This is only supposed to happen during local builds. Since this build system is
# supposed to run side by side--machine and then user--we do NOT want to destroy
# the .wxs files. Therefore, we pass RunBuildEvents=false to suppress all of that
# logic.
#
# We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
# We only pass -restore on the first one because the second run should already have all
# of the dependencies.
- task: VSBuild@1
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext MSI
displayName: 💻 Build VNext MSI
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
-restore
/t:PowerToysInstallerVNext
/p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog
/p:RunBuildEvents=false;PerUser=false;BuildProjectReferences=false;CIBuild=true
/bl:$(LogOutputDirectory)\installer-machine-msi.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
clean: false # don't undo our hard work above by deleting the CustomActions dll
msbuildArchitecture: x64
maximumCpuCount: true
- task: VSBuild@1
displayName: 👤 Build VNext MSI
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysInstallerVNext
/p:RunBuildEvents=false;PerUser=true;BuildProjectReferences=false;CIBuild=true
/bl:$(LogOutputDirectory)\installer-user-msi.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -100,77 +105,66 @@ steps:
maximumCpuCount: true
- script: |-
wix msi decompile installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).msi -x $(build.sourcesdirectory)\extractedMsi
dir $(build.sourcesdirectory)\extractedMsi
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract and verify MSI"
wix msi decompile $(InstallerMachineRoot)\$(InstallerMachineBasename).msi -x $(build.sourcesdirectory)\extractedMachineMsi
wix msi decompile $(InstallerUserRoot)\$(InstallerUserBasename).msi -x $(build.sourcesdirectory)\extractedUserMsi
dir $(build.sourcesdirectory)\extractedMachineMsi
dir $(build.sourcesdirectory)\extractedUserMsi
displayName: "WiX5: Extract and verify MSIs"
# Check if deps.json files don't reference different dll versions.
- pwsh: |-
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Audit deps.json in MSI extracted files
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
displayName: Audit deps.json in MSI extracted files
- ${{ if eq(parameters.codeSign, true) }}:
- pwsh: |-
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary'
git clean -xfd ./extractedMsi
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Verify all binaries are signed and versioned
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\Binary'
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
& .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\Binary'
git clean -xfd ./extractedMachineMsi ./extractedUserMsi
displayName: Verify all binaries are signed and versioned
- template: steps-esrp-signing.yml
- template: steps-esrp-sign-files-authenticode.yml
parameters:
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign VNext MSI
displayName: Sign VNext MSIs
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
signType: batchSigning
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
folder: 'installer'
pattern: '**/PowerToys*Setup-*.msi'
#### END MSI
#### BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
- task: VSBuild@1
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build SilentFilesInUseBAFunction
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
/t:SilentFilesInUseBAFunction
/p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
-restore -graph
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-SilentFilesInUseBAFunction.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
clean: false # don't undo our hard work above by deleting the msi
msbuildArchitecture: x64
maximumCpuCount: true
- ${{ if eq(parameters.codeSign, true) }}:
- template: steps-esrp-signing.yml
parameters:
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign SilentFilesInUseBAFunction
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: 'installer/$(BuildPlatform)/$(BuildConfiguration)'
signType: batchSigning
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
#### END BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
#### BOOTSTRAP BUILDING AND SIGNING
# We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
# We only pass -restore on the first one because the second run should already have all
# of the dependencies.
- task: VSBuild@1
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext Bootstrapper
displayName: 💻 Build VNext Bootstrapper
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
-restore
/t:PowerToysBootstrapperVNext
/p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog
-restore -graph
/p:PerUser=false;BuildProjectReferences=false;CIBuild=true
/bl:$(LogOutputDirectory)\installer-machine-bootstrapper.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
clean: false # don't undo our hard work above by deleting the MSI nor SilentFilesInUseBAFunction
msbuildArchitecture: x64
maximumCpuCount: true
- task: VSBuild@1
displayName: 👤 Build VNext Bootstrapper
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysBootstrapperVNext
/p:PerUser=true;BuildProjectReferences=false;CIBuild=true
/bl:$(LogOutputDirectory)\installer-user-bootstrapper.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -181,54 +175,41 @@ steps:
# The entirety of bundle unpacking/re-packing is unnecessary if we are not code signing it.
- ${{ if eq(parameters.codeSign, true) }}:
- script: |-
wix burn detach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract Engine from Bundle"
wix burn detach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe
wix burn detach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe
displayName: "WiX5: Extract Engines from Bundles"
- template: steps-esrp-signing.yml
- template: steps-esrp-sign-files-authenticode.yml
parameters:
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign WiX Engine
displayName: Sign WiX Engines
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: "installer"
Pattern: engine.exe
signConfigType: inlineSignParams
inlineOperation: |
[
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolSign",
"Parameters": {
"OpusName": "Microsoft",
"OpusInfo": "http://www.microsoft.com",
"FileDigest": "/fd \"SHA256\"",
"PageHash": "/NPH",
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
},
"ToolName": "sign",
"ToolVersion": "1.0"
},
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolVerify",
"Parameters": {},
"ToolName": "sign",
"ToolVersion": "1.0"
}
]
folder: "installer"
pattern: '*-engine.exe'
- script: |-
wix burn reattach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe -o installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe
displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Reattach Engine to Bundle"
wix burn reattach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe -o $(InstallerMachineRoot)\$(InstallerMachineBasename).exe
wix burn reattach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe -o $(InstallerUserRoot)\$(InstallerUserBasename).exe
displayName: "WiX5: Reattach Engines to Bundles"
- template: steps-esrp-signing.yml
- pwsh: |-
& wix burn extract -oba installer\ba\m "$(InstallerMachineRoot)\$(InstallerMachineBasename).exe"
& wix burn extract -oba installer\ba\u "$(InstallerUserRoot)\$(InstallerUserBasename).exe"
Get-ChildItem installer\ba -Recurse -Include *.exe,*.dll | Get-AuthenticodeSignature | ForEach-Object {
If ($_.Status -Ne "Valid") {
Write-Error $_.StatusMessage
} Else {
Write-Host $_.StatusMessage
}
}
& git clean -fdx installer\ba
displayName: "WiX5: Verify Bootstrapper content is signed"
- template: steps-esrp-sign-files-authenticode.yml
parameters:
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign Final Bootstrapper
displayName: Sign Final Bootstrappers
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
signType: batchSigning
batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
folder: 'installer'
pattern: '**/PowerToys*Setup-*.exe'
#### END BOOTSTRAP
## END INSTALLER

View File

@@ -0,0 +1,45 @@
parameters:
- name: displayName
type: string
default: Sign Specific Files
- name: folder
type: string
- name: pattern
type: string
- name: signingIdentity
type: object
default: {}
steps:
- template: steps-esrp-signing.yml
parameters:
displayName: ${{ parameters.displayName }}
signingIdentity: ${{ parameters.signingIdentity }}
inputs:
FolderPath: ${{ parameters.folder }}
Pattern: ${{ parameters.pattern }}
UseMinimatch: true
signConfigType: inlineSignParams
inlineOperation: |-
[
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolSign",
"Parameters": {
"OpusName": "Microsoft",
"OpusInfo": "http://www.microsoft.com",
"FileDigest": "/fd \"SHA256\"",
"PageHash": "/NPH",
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
},
"ToolName": "sign",
"ToolVersion": "1.0"
},
{
"KeyCode": "CP-230012",
"OperationCode": "SigntoolVerify",
"Parameters": {},
"ToolName": "sign",
"ToolVersion": "1.0"
}
]

View File

@@ -34,12 +34,8 @@
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir Condition=" '$(PerUser)' != 'true' ">$(Platform)\$(Configuration)\MachineSetup\</OutDir>
<OutDir Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup\</OutDir>
<IntDir Condition=" '$(PerUser)' != 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\</IntDir>
<IntDir Condition=" '$(PerUser)' == 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\</IntDir>
<NormalizedPerUserValue>false</NormalizedPerUserValue>
<NormalizedPerUserValue Condition=" '$(PerUser)' == 'true' ">true</NormalizedPerUserValue>
<OutDir>$(Platform)\$(Configuration)\SetupShared\</OutDir>
<IntDir>$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\SetupShared\obj\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
@@ -79,8 +75,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs.bk""""
if not "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
if "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(NormalizedPerUserValue)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
</Command>
<Message>Backing up original files and populating .NET and WPF Runtime dependencies for WiX3 based installer</Message>
</PreBuildEvent>
@@ -178,4 +173,4 @@
<Error Condition="!Exists('..\packages\WixToolset.DUtil.5.0.2\build\WixToolset.DUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.DUtil.5.0.2\build\WixToolset.DUtil.props'))" />
<Error Condition="!Exists('..\packages\WixToolset.WcaUtil.5.0.2\build\WixToolset.WcaUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.WcaUtil.5.0.2\build\WixToolset.WcaUtil.props'))" />
</Target>
</Project>
</Project>

View File

@@ -4,6 +4,13 @@
<Fragment>
<DirectoryRef Id="WinUI3AppsInstallFolder">
<Directory Id="CmdPalInstallFolder" Name="CmdPal">
<Directory Id="CmdPalDepsInstallFolder" Name="Dependencies">
<?if $(sys.BUILDARCH) = x64 ?>
<Directory Id="CmdPalDepsX64InstallFolder" Name="x64" />
<?else?>
<Directory Id="CmdPalDepsArm64InstallFolder" Name="arm64" />
<?endif?>
</Directory>
</Directory>
</DirectoryRef>
<DirectoryRef Id="CmdPalInstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test">
@@ -18,14 +25,40 @@
<?endif?>
</Component>
</DirectoryRef>
<?if $(sys.BUILDARCH) = x64 ?>
<DirectoryRef Id="CmdPalDepsX64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64">
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.VCLibs.x64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?else?>
<DirectoryRef Id="CmdPalDepsArm64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64">
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.VCLibs.ARM64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64\Microsoft.VCLibs.ARM64.14.00.Desktop.appx" />
</Component>
</DirectoryRef>
<?endif?>
<ComponentGroup Id="CmdPalComponentGroup">
<Component Id="RemoveCmdPalFolder" Guid="2DF90C08-CC75-4245-A14E-B82904636C53" Directory="INSTALLFOLDER">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveCmdPalFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveCmdPalInstallDirFolder" Directory="CmdPalInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveCmdPalDepsInstallDirFolder" Directory="CmdPalDepsInstallFolder" On="uninstall" />
<?if $(sys.BUILDARCH) = x64 ?>
<RemoveFolder Id="RemoveCmdPalDepsX64InstallDirFolder" Directory="CmdPalDepsX64InstallFolder" On="uninstall" />
<?else?>
<RemoveFolder Id="RemoveCmdPalDepsArm64InstallDirFolder" Directory="CmdPalDepsArm64InstallFolder" On="uninstall" />
<?endif?>
</Component>
<ComponentRef Id="Module_CmdPal" />
<ComponentRef Id="Module_CmdPal_Deps" />
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -60,6 +60,12 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs
</PostBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(RunBuildEvents)'=='false'">
<PostBuildEvent></PostBuildEvent>
<RunPostBuildEvent></RunPostBuildEvent>
<PreBuildEventUseInBuild>false</PreBuildEventUseInBuild>
<PostBuildEventUseInBuild>false</PostBuildEventUseInBuild>
</PropertyGroup>
<PropertyGroup Label="UserMacros" Condition=" '$(PerUser)' == 'true' ">
<DefineConstants>$(DefineConstants);PerUser=true</DefineConstants>
</PropertyGroup>

View File

@@ -1,9 +1,7 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
[string]$platform,
[Parameter(Mandatory = $False, Position = 2)]
[string]$installscopeperuser = "false"
[string]$platform
)
Function Generate-FileList() {
@@ -77,9 +75,7 @@ Function Generate-FileComponents() {
[Parameter(Mandatory = $True, Position = 1)]
[string]$fileListName,
[Parameter(Mandatory = $True, Position = 2)]
[string]$wxsFilePath,
[Parameter(Mandatory = $True, Position = 3)]
[string]$regroot
[string]$wxsFilePath
)
$wxsFile = Get-Content $wxsFilePath;
@@ -100,7 +96,7 @@ Function Generate-FileComponents() {
$componentDefs +=
@"
<Component Id="$($componentId)" Guid="$((New-Guid).ToString().ToUpper())">
<RegistryKey Root="$($regroot)" Key="Software\Classes\powertoys\components">
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="$($componentId)" Value="" KeyPath="yes"/>
</RegistryKey>`r`n
"@
@@ -134,194 +130,188 @@ if ($platform -ceq "arm64") {
$platform = "ARM64"
}
if ($installscopeperuser -eq "true") {
$registryroot = "HKCU"
} else {
$registryroot = "HKLM"
}
#BaseApplications
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot
Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
#WinUI3Applications
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
#AdvancedPaste
Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot
Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs
#AwakeFiles
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot
Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs
#ColorPicker
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs
#Environment Variables
Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot
Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs
#FileExplorerAdd-ons
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
#FileLocksmith
Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith"
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot
Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs
#Hosts
Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts"
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot
Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs
#ImageResizer
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
# Light Switch Service
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
#Peek
Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\"
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot
Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs
#PowerRename
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
#RegistryPreview
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs
#Run
Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher"
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
## Plugins
###Calculator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images"
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Folder
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images"
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Program
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images"
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Shell
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images"
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Indexer
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images"
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###UnitConverter
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images"
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WebSearch
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images"
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###History
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images"
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Uri
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images"
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###VSCodeWorkspaces
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images"
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowWalker
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images"
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###OneNote
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images"
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Registry
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images"
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Service
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images"
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###System
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images"
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###TimeDate
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images"
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowsSettings
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images"
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowsTerminal
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images"
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###PowerToys
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images"
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###ValueGenerator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images"
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
## Plugins
#ShortcutGuide
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
#Settings
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
#Workspaces
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs

View File

@@ -26,6 +26,16 @@ namespace ManagedCommon
private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
/// <summary>
/// Gets the path to the log directory for the current version of the app.
/// </summary>
public static string CurrentVersionLogDirectoryPath { get; private set; }
/// <summary>
/// Gets the path to the log directory for the app.
/// </summary>
public static string AppLogDirectoryPath { get; private set; }
/// <summary>
/// Initializes the logger and sets the path for logging.
/// </summary>
@@ -42,6 +52,9 @@ namespace ManagedCommon
Directory.CreateDirectory(versionedPath);
}
AppLogDirectoryPath = basePath;
CurrentVersionLogDirectoryPath = versionedPath;
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));

View File

@@ -92,6 +92,7 @@
<ClInclude Include="..\NewShellExtensionContextMenu\template_folder.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\template_item.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\trace.h" />
<ClInclude Include="..\NewShellExtensionContextMenu\Helpers.h" />
<ClInclude Include="dll_main.h" />
<ClInclude Include="Generated Files\resource.h" />
<ClInclude Include="pch.h" />
@@ -99,7 +100,7 @@
<ClInclude Include="shell_context_menu_win10.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\Helpers.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\new_utilities.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\powertoys_module.cpp" />
<ClCompile Include="..\NewShellExtensionContextMenu\settings.cpp" />

View File

@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM

View File

@@ -0,0 +1,118 @@
// 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.
#include "pch.h"
#include "Helpers.h"
#include <regex>
// Minimal subset of PowerRename Helpers used by NewPlus
// This is a copy from PowerRename main branch to avoid cross-module dependencies
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
{
std::locale::global(std::locale(""));
HRESULT hr = E_INVALIDARG;
if (source && wcslen(source) > 0)
{
std::wstring res(source);
wchar_t replaceTerm[MAX_PATH] = { 0 };
wchar_t formattedDate[MAX_PATH] = { 0 };
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) == 0)
{
StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US");
}
int hour12 = (fileTime.wHour % 12);
if (hour12 == 0)
{
hour12 = 12;
}
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm);
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm");
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
hr = StringCchCopy(result, cchMax, res.c_str());
}
return hr;
}

View File

@@ -0,0 +1,9 @@
// 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.
#pragma once
// Minimal subset of PowerRename Helpers used by NewPlus
// This is a copy from PowerRename's main branch to avoid cross-module dependencies
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);

View File

@@ -110,6 +110,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="dll_main.h" />
<ClInclude Include="Helpers.h" />
<ClInclude Include="helpers_filesystem.h" />
<ClInclude Include="helpers_variables.h" />
<ClInclude Include="shell_context_menu.h" />
@@ -127,7 +128,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ClInclude Include="template_item.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\powerrename\lib\Helpers.cpp" />
<ClCompile Include="Helpers.cpp" />
<ClCompile Include="new_utilities.cpp" />
<ClCompile Include="shell_context_menu.cpp" />
<ClCompile Include="shell_context_sub_menu.cpp" />

View File

@@ -1,7 +1,7 @@
#pragma once
#include <regex>
#include "..\..\powerrename\lib\Helpers.h"
#include "Helpers.h"
#include "helpers_filesystem.h"
#pragma comment(lib, "Pathcch.lib")

View File

@@ -302,9 +302,9 @@ namespace newplus::utilities
POINT mouse_position;
GetCursorPos(&mouse_position);
mouse_position.x -= GetSystemMetrics(SM_CXMENUSIZE);
mouse_position.x = max(mouse_position.x, 20);
mouse_position.x = (std::max)(mouse_position.x, 20L);
mouse_position.y -= GetSystemMetrics(SM_CXMENUSIZE)/2;
mouse_position.y = max(mouse_position.y, 20);
mouse_position.y = (std::max)(mouse_position.y, 20L);
POINT position[] = { mouse_position };
folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM);
}

View File

@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM
@@ -13,6 +14,7 @@
#include <shellapi.h>
#include <Windows.h>
#include <shlobj.h>
#include <algorithm>
#include <vector>
#include <system_error>
#include <memory>

View File

@@ -60,8 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
{
filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size()));
filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size()));
filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size()));
filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size()));
return filename;
}

View File

@@ -2,8 +2,10 @@
// 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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Interactions;
using static Microsoft.PowerToys.UITest.UITestBase;
namespace PowerOCR.UITests;
@@ -19,41 +21,274 @@ public class PowerOCRTests : UITestBase
[TestInitialize]
public void TestInitialize()
{
if (FindAll<NavigationViewItem>("Text Extractor").Count == 0)
if (FindAll<NavigationViewItem>(By.AccessibilityId("TextExtractorNavItem")).Count == 0)
{
// Expand Advanced list-group if needed
Find<NavigationViewItem>("System Tools").Click();
// Expand System Tools list-group if needed
Find<NavigationViewItem>(By.AccessibilityId("SystemToolsNavItem")).Click();
}
Find<NavigationViewItem>("Text Extractor").Click();
Find<NavigationViewItem>(By.AccessibilityId("TextExtractorNavItem")).Click();
Find<ToggleSwitch>("Enable Text Extractor").Toggle(true);
Find<ToggleSwitch>(By.AccessibilityId("EnableTextExtractorToggleSwitch")).Toggle(true);
SendKeys(Key.Win, Key.D);
// Reset activation shortcut to default (Win+Shift+T)
var shortcutControl = Find<Element>(By.AccessibilityId("TextExtractorActivationShortcut"), 5000);
if (shortcutControl != null)
{
shortcutControl.Click();
Thread.Sleep(500);
// Set default shortcut Win+Shift+T
SendKeys(Key.Win, Key.Shift, Key.T);
Thread.Sleep(1000);
// Click Save to confirm
var saveButton = Find<Button>(By.Name("Save"), 3000);
if (saveButton != null)
{
saveButton.Click();
Thread.Sleep(1000);
}
}
}
[TestMethod("PowerOCR.DetectTextExtractor")]
[TestCategory("PowerOCR Detection")]
public void DetectTextExtractorTest()
{
try
// Step 1: Press the activation shortcut and verify the overlay appears
SendKeys(Key.Win, Key.Shift, Key.T);
var textExtractorWindow = Find<Element>(By.AccessibilityId("TextExtractorWindow"), 10000, true);
Assert.IsNotNull(textExtractorWindow, "TextExtractor window should be found after hotkey activation");
// Step 2: Press Escape and verify the overlay disappears
SendKeys(Key.Esc);
Thread.Sleep(3000);
var windowsAfterEscape = FindAll<Element>(By.AccessibilityId("TextExtractorWindow"), 2000, true);
Assert.AreEqual(0, windowsAfterEscape.Count, "TextExtractor window should be dismissed after pressing Escape");
// Step 3: Press the activation shortcut again and verify the overlay appears
SendKeys(Key.Win, Key.Shift, Key.T);
textExtractorWindow = Find<Element>(By.AccessibilityId("TextExtractorWindow"), 10000, true);
Assert.IsNotNull(textExtractorWindow, "TextExtractor window should appear again after hotkey activation");
// Step 4: Right-click and select Cancel. Verify the overlay disappears
textExtractorWindow.Click(rightClick: true);
Thread.Sleep(500);
// Look for Cancel menu item using its AutomationId
var cancelMenuItem = Find<Element>(By.AccessibilityId("CancelMenuItem"), 3000, true);
Assert.IsNotNull(cancelMenuItem, "Cancel menu item should be available in context menu");
cancelMenuItem.Click();
Thread.Sleep(3000);
var windowsAfterCancel = FindAll<Element>(By.AccessibilityId("TextExtractorWindow"), 2000, true);
Assert.AreEqual(0, windowsAfterCancel.Count, "TextExtractor window should be dismissed after clicking Cancel");
}
[TestMethod("PowerOCR.DisableTextExtractorTest")]
[TestCategory("PowerOCR Settings")]
public void DisableTextExtractorTest()
{
Find<ToggleSwitch>(By.AccessibilityId("EnableTextExtractorToggleSwitch")).Toggle(false);
SendKeys(Key.Win, Key.Shift, Key.T);
// Verify that no TextExtractor window appears
var windowsWhenDisabled = FindAll<Element>(By.AccessibilityId("TextExtractorWindow"), 10000, true);
Assert.AreEqual(0, windowsWhenDisabled.Count, "TextExtractor window should not appear when the utility is disabled");
}
[TestMethod("PowerOCR.ActivationShortcutSettingsTest")]
[TestCategory("PowerOCR Settings")]
public void ActivationShortcutSettingsTest()
{
// Find the activation shortcut control
var shortcutControl = Find<Element>(By.AccessibilityId("TextExtractorActivationShortcut"), 5000);
Assert.IsNotNull(shortcutControl, "Activation shortcut control should be found");
// Click to focus the shortcut control
shortcutControl.Click();
Thread.Sleep(500);
// Test changing the shortcut to Ctrl+Shift+T
SendKeys(Key.Ctrl, Key.Shift, Key.T);
Thread.Sleep(1000);
// Click the Save button to confirm the shortcut change
var saveButton = Find<Button>(By.Name("Save"), 3000);
Assert.IsNotNull(saveButton, "Save button should be found in the shortcut dialog");
saveButton.Click();
Thread.Sleep(1000);
// Test the new shortcut
SendKeys(Key.Ctrl, Key.Shift, Key.T);
Thread.Sleep(3000);
var textExtractorWindow = FindAll<Element>(By.AccessibilityId("TextExtractorWindow"), 3000, true);
Assert.IsTrue(textExtractorWindow.Count > 0, "TextExtractor should activate with new shortcut Ctrl+Shift+T");
}
[TestMethod("PowerOCR.OCRLanguageSettingsTest")]
[TestCategory("PowerOCR Settings")]
public void OCRLanguageSettingsTest()
{
// Find the language combo box
var languageComboBox = Find<ComboBox>(By.AccessibilityId("TextExtractorLanguageComboBox"), 5000);
Assert.IsNotNull(languageComboBox, "Language combo box should be found");
// Click to open the dropdown
languageComboBox.Click();
// Verify dropdown is opened by checking if dropdown items are available
var dropdownItems = FindAll<Element>(By.ClassName("ComboBoxItem"), 2000);
Assert.IsTrue(dropdownItems.Count > 0, "Dropdown should contain language options");
// Close dropdown by pressing Escape
SendKeys(Key.Esc);
}
[TestMethod("PowerOCR.OCRLanguageSelectionTest")]
[TestCategory("PowerOCR Language")]
public void OCRLanguageSelectionTest()
{
// Activate Text Extractor overlay
SendKeys(Key.Win, Key.Shift, Key.T);
Thread.Sleep(3000);
var textExtractorWindow = Find<Element>(By.AccessibilityId("TextExtractorWindow"), 10000, true);
Assert.IsNotNull(textExtractorWindow, "TextExtractor window should be found after hotkey activation");
// Right-click on the canvas to open context menu
textExtractorWindow.Click(rightClick: true);
// Look for language options that should appear after Cancel menu item
var allMenuItems = FindAll<Element>(By.ClassName("MenuItem"), 2000, true);
if (allMenuItems.Count > 4)
{
SendKeys(Key.Win, Key.Shift, Key.T);
// Find the Cancel menu item first
Element? cancelItem = null;
int cancelIndex = -1;
for (int i = 0; i < allMenuItems.Count; i++)
{
if (allMenuItems[i].GetAttribute("AutomationId") == "CancelMenuItem")
{
cancelItem = allMenuItems[i];
cancelIndex = i;
break;
}
}
Thread.Sleep(5000);
Assert.IsNotNull(cancelItem, "Cancel menu item should be found");
var textExtractorWindow = Find("TextExtractor", 10000, true);
// Look for language options after Cancel menu item
if (cancelIndex >= 0 && cancelIndex < allMenuItems.Count - 1)
{
// Select the first language option after Cancel
var languageOption = allMenuItems[cancelIndex + 1];
languageOption.Click();
Thread.Sleep(1000);
Assert.IsNotNull(textExtractorWindow, "TextExtractor window should be found after hotkey activation");
Console.WriteLine("✓ TextExtractor window detected successfully after hotkey activation");
SendKeys(Key.Esc);
Assert.IsTrue(true, "Language selection changed successfully through right-click menu");
}
}
catch (Exception ex)
// Close the TextExtractor overlay
SendKeys(Key.Esc);
Thread.Sleep(1000);
}
[TestMethod("PowerOCR.TextSelectionAndClipboardTest")]
[TestCategory("PowerOCR Selection")]
public void TextSelectionAndClipboardTest()
{
// Clear clipboard first using STA thread
ClearClipboardSafely();
Thread.Sleep(500);
// Activate Text Extractor overlay
SendKeys(Key.Win, Key.Shift, Key.T);
Thread.Sleep(3000);
var textExtractorWindow = Find<Element>(By.AccessibilityId("TextExtractorWindow"), 10000, true);
Assert.IsNotNull(textExtractorWindow, "TextExtractor window should be found after hotkey activation");
// Click on the TextExtractor window to position cursor
textExtractorWindow.Click();
Thread.Sleep(500);
// Get screen dimensions for full screen selection
var primaryScreen = System.Windows.Forms.Screen.PrimaryScreen;
Assert.IsNotNull(primaryScreen, "Primary screen should be available");
var screenWidth = primaryScreen.Bounds.Width;
var screenHeight = primaryScreen.Bounds.Height;
// Define full screen selection area
var startX = 0;
var startY = 0;
var endX = screenWidth;
var endY = screenHeight;
// Perform continuous mouse drag to select entire screen
PerformSeleniumDrag(startX, startY, endX, endY);
Thread.Sleep(3000); // Wait longer for full screen OCR processing
// Verify text was copied to clipboard using STA thread
var clipboardText = GetClipboardTextSafely();
Assert.IsFalse(string.IsNullOrWhiteSpace(clipboardText), "Clipboard should contain extracted text after selection");
// Close the TextExtractor overlay
SendKeys(Key.Esc);
Thread.Sleep(1000);
}
private static void ClearClipboardSafely()
{
var thread = new System.Threading.Thread(() =>
{
Console.WriteLine($"Failed to detect TextExtractor window: {ex.Message}");
Assert.Fail("TextExtractor window was not found after hotkey activation");
}
System.Windows.Forms.Clipboard.Clear();
});
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();
thread.Join();
}
private static string GetClipboardTextSafely()
{
string result = string.Empty;
var thread = new System.Threading.Thread(() =>
{
try
{
result = System.Windows.Forms.Clipboard.GetText();
}
catch (Exception)
{
result = string.Empty;
}
});
thread.SetApartmentState(System.Threading.ApartmentState.STA);
thread.Start();
thread.Join();
return result;
}
private void PerformSeleniumDrag(int startX, int startY, int endX, int endY)
{
// Use Selenium Actions for proper drag and drop operation
var actions = new Actions(Session.Root);
// Move to start position, click and hold, drag to end position, then release
actions.MoveByOffset(startX, startY)
.ClickAndHold()
.MoveByOffset(endX - startX, endY - startY)
.Release()
.Build()
.Perform();
}
}

View File

@@ -0,0 +1,15 @@
## Text Extractor
* Enable Text Extractor. Then:
- [x] Press the activation shortcut and verify the overlay appears.
- [x] Press Escape and verify the overlay disappears.
- [x] Press the activation shortcut and verify the overlay appears.
- [x] Right-click and select Cancel. Verify the overlay disappears.
- [x] Disable Text Extractor and verify that the activation shortcut no longer activates the utility.
* With Text Extractor enabled and activated:
- [x] Try to select text and verify it is copied to the clipboard.
- [x] Try to select a different OCR language by right-clicking and verify the change is applied.
* In a multi-monitor setup with different DPIs on each monitor:
- [ ] Verify text is correctly captured on all monitors.
* Test the different settings and verify they are applied:
- [x] Activation shortcut
- [x] OCR Language

View File

@@ -6,6 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:PowerOCR.Properties"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
x:Name="TextExtractorWindow"
Title="TextExtractor"
ui:Design.Background="Transparent"
AllowsTransparency="True"
@@ -87,6 +88,7 @@
<Separator />
<MenuItem
Name="CancelMenuItem"
AutomationProperties.AutomationId="CancelMenuItem"
Click="CancelMenuItem_Click"
Header="{x:Static p:Resources.Cancel}" />
</ContextMenu>
@@ -117,6 +119,7 @@
<ComboBox
x:Name="LanguagesComboBox"
Margin="4,0"
AutomationProperties.AutomationId="OCROverlayLanguagesComboBox"
AutomationProperties.Name="{x:Static p:Resources.SelectedLang}"
SelectionChanged="LanguagesComboBox_SelectionChanged">
<ComboBox.ItemTemplate>

View File

@@ -40,6 +40,8 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public partial class App : Application
{
private readonly GlobalErrorHandler _globalErrorHandler = new();
/// <summary>
/// Gets the current <see cref="App"/> instance in use.
/// </summary>
@@ -61,6 +63,10 @@ public partial class App : Application
/// </summary>
public App()
{
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
_globalErrorHandler.Register(this);
#endif
Services = ConfigureServices();
this.InitializeComponent();

View File

@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs;
using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs;
namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Global error handler for Command Palette.
/// </summary>
internal sealed partial class GlobalErrorHandler
{
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
internal void Register(App app)
{
ArgumentNullException.ThrowIfNull(app);
app.UnhandledException += App_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
}
private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e)
{
// Exceptions thrown on the main UI thread are handled here.
if (e.Exception != null)
{
HandleException(e.Exception, Context.MainThreadException);
}
}
private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e)
{
// Exceptions thrown on background threads are handled here.
if (e.ExceptionObject is Exception ex)
{
HandleException(ex, Context.AppDomainUnhandledException);
}
}
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
// This event is raised only when a faulted Task is garbage-collected
// without its exception being observed. It is NOT raised immediately
// when the Task faults; timing depends on GC finalization.
e.SetObserved();
HandleException(e.Exception, Context.UnobservedTaskException, isRecoverable: true);
}
private void HandleException(Exception ex, Context context, bool isRecoverable = false)
{
Logger.LogError($"Unhandled exception detected ({context})", ex);
if (context == Context.MainThreadException)
{
var error = DiagnosticsHelper.BuildExceptionMessage(ex, null);
var report = $"""
This is an error report generated by Windows Command Palette.
If you are seeing this message, it means the application has encountered an unexpected issue.
You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
{error}
""";
StoreReport(report, storeOnDesktop: false);
PInvoke.MessageBox(
HWND.Null,
"Command Palette has encountered a fatal error and must close.\n\nAn error report has been saved to your desktop.",
"Unhandled Error",
MESSAGEBOX_STYLE.MB_ICONERROR);
}
}
private static string? StoreReport(string report, bool storeOnDesktop)
{
// Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions
// in case of crash storm.
var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log");
// Always store a copy in log directory, this way it is available for Bug Report Tool
string? reportPath = null;
if (Logger.CurrentVersionLogDirectoryPath != null)
{
reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath);
}
// Optionally store a copy on the desktop for user (in)convenience
if (storeOnDesktop)
{
var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory));
// show the desktop copy if both succeeded
if (path != null)
{
reportPath = path;
}
}
return reportPath;
static string? Save(string reportContent, string reportFileName, Func<string> directory)
{
try
{
var logDirectory = directory();
Directory.CreateDirectory(logDirectory);
var reportFilePath = Path.Combine(logDirectory, reportFileName);
File.WriteAllText(reportFilePath, reportContent);
return reportFilePath;
}
catch (Exception ex)
{
Logger.LogError("Failed to store exception report", ex);
return null;
}
}
}
private enum Context
{
Unknown = 0,
MainThreadException,
BackgroundThreadException,
UnobservedTaskException,
AppDomainUnhandledException,
}
}

View File

@@ -111,6 +111,10 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<SDKReference Include="Microsoft.VCLibs.Desktop, Version=14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />

View File

@@ -110,7 +110,7 @@ public partial class BookmarkResolverTests
[
new PlaceholderClassificationCase(
Name: "Drive",
Input: "C:",
Input: "C:\\.",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: "C:\\",

View File

@@ -52,7 +52,7 @@
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>legacy_stdio_definitions.lib;windowscodecs.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"</Command>
@@ -71,6 +71,7 @@
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalDependencies>windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>

View File

@@ -51,7 +51,7 @@
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>runtimeobject.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
</Link>
<PreBuildEvent>
@@ -75,7 +75,7 @@ MakeAppx.exe pack /d . /p $(OutDir)PowerRenameContextMenuPackage.msix /nv</Comma
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>runtimeobject.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
</Link>
<PreBuildEvent>

View File

@@ -79,7 +79,7 @@
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
@@ -89,7 +89,7 @@
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>

View File

@@ -1,4 +1,4 @@
#include "pch.h"
#include "pch.h"
#include "App.xaml.h"
#include "MainWindow.xaml.h"
@@ -117,6 +117,9 @@ App::App()
/// <param name="e">Details about the launch request and process.</param>
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
// WinUI3 framework automatically initializes COM as STA on the main thread
// No manual initialization needed for WIC operations
LoggerHelpers::init_logger(moduleName, L"", LogSettings::powerRenameLoggerName);
if (powertoys_gpo::getConfiguredPowerRenameEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
@@ -237,7 +240,6 @@ void App::OnLaunched(LaunchActivatedEventArgs const&)
}
#else
#define BUFSIZE 4096 * 4
BOOL bSuccess;
WCHAR chBuf[BUFSIZE];
DWORD dwRead;
@@ -269,4 +271,4 @@ void App::OnLaunched(LaunchActivatedEventArgs const&)
window = make<MainWindow>();
window.Activate();
}
}

View File

@@ -16,6 +16,7 @@ namespace PowerRenameUI
Windows.Foundation.Collections.IObservableVector<PatternSnippet> DateTimeShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> CounterShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> RandomizerShortcuts { get; };
Windows.Foundation.Collections.IObservableVector<PatternSnippet> MetadataShortcuts { get; };
String OriginalCount;
String RenamedCount;

View File

@@ -330,6 +330,8 @@
<RowDefinition Height="*" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
<RowDefinition Height="28" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock x:Uid="DateTimeCheatSheet_Title" FontWeight="SemiBold" />
<ListView
@@ -451,6 +453,48 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Media Metadata -->
<TextBlock
x:Uid="MetadataCheatSheet_Title"
Grid.Row="6"
Margin="0,10,0,0"
FontWeight="SemiBold" />
<ListView
Grid.Row="7"
Margin="-4,12,0,0"
IsItemClickEnabled="True"
ItemClick="MetadataItemClick"
ItemsSource="{x:Bind MetadataShortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:PatternSnippet">
<Grid Margin="-10,0,0,0" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Padding="8"
HorizontalAlignment="Left"
Background="{ThemeResource ButtonBackground}"
BorderBrush="{ThemeResource ButtonBorderBrush}"
BorderThickness="1"
CornerRadius="4">
<TextBlock
FontFamily="Consolas"
Foreground="{ThemeResource ButtonForeground}"
Text="{x:Bind Code}" />
</Border>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Description}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Flyout>
</Button.Flyout>
@@ -560,31 +604,61 @@
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
</StackPanel>
<TextBlock
x:Name="FileTimeLabel"
x:Uid="TextBlock_FileTime"
Margin="0,16,0,8"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,16,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="12" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- File Time Section -->
<TextBlock
x:Name="FileTimeLabel"
x:Uid="TextBlock_FileTime"
Grid.Row="0"
Grid.Column="0"
Margin="0,0,0,8"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<StackPanel
Grid.Row="2"
Grid.Column="1"
Orientation="Horizontal"
Spacing="4">
<ComboBox
x:Name="comboBox_fileTimeParts"
Grid.Row="1"
Grid.Column="0"
Width="200"
HorizontalAlignment="Stretch"
AutomationProperties.LabeledBy="{Binding ElementName=FileDateLabel}"
AutomationProperties.LabeledBy="{Binding ElementName=FileTimeLabel}"
SelectedIndex="0">
<ComboBoxItem x:Uid="FileTimeParts_CreationTime" />
<ComboBoxItem x:Uid="FileTimeParts_ModificationTime" />
<ComboBoxItem x:Uid="FileTimeParts_AccessTime" />
</ComboBox>
</StackPanel>
<!-- Metadata Source Section -->
<TextBlock
x:Name="MetadataSourceLabel"
x:Uid="TextBlock_MetadataSource"
Grid.Row="0"
Grid.Column="2"
Margin="0,0,0,8"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ComboBox
x:Name="comboBox_metadataSource"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch"
AutomationProperties.LabeledBy="{Binding ElementName=MetadataSourceLabel}"
SelectedIndex="0"
SelectionChanged="MetadataSourceComboBox_SelectionChanged">
<ComboBoxItem x:Uid="MetadataSource_EXIF" />
<ComboBoxItem x:Uid="MetadataSource_XMP" />
</ComboBox>
</Grid>
</StackPanel>

View File

@@ -1,4 +1,4 @@
#include "pch.h"
#include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
@@ -6,6 +6,7 @@
#include <settings.h>
#include <trace.h>
#include <Helpers.h>
#include <common/logger/call_tracer.h>
#include <common/logger/logger.h>
@@ -225,6 +226,11 @@ namespace winrt::PowerRenameUI::implementation
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${rstringdigit=36}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Digit").ValueAsString()));
m_RandomizerShortcuts.Append(winrt::make<PatternSnippet>(L"${ruuidv4}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Uuid").ValueAsString()));
// Initialize metadata shortcuts - will be populated based on selected metadata type
m_metadataShortcuts = winrt::single_threaded_observable_vector<PowerRenameUI::PatternSnippet>();
// Initialize with EXIF patterns (default)
UpdateMetadataShortcuts(PowerRenameLib::MetadataType::EXIF);
InitializeComponent();
m_etwTrace.UpdateState(true);
@@ -356,7 +362,10 @@ namespace winrt::PowerRenameUI::implementation
hstring MainWindow::OriginalCount()
{
UINT count = 0;
m_prManager->GetItemCount(&count);
if (m_prManager)
{
m_prManager->GetItemCount(&count);
}
return hstring{ std::to_wstring(count) };
}
@@ -394,13 +403,16 @@ namespace winrt::PowerRenameUI::implementation
button_showAll().IsChecked(true);
button_showRenamed().IsChecked(false);
DWORD filter = 0;
m_prManager->GetFilter(&filter);
if (filter != PowerRenameFilters::None)
if (m_prManager)
{
m_prManager->SwitchFilter(0);
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(false);
InvalidateItemListViewState();
DWORD filter = 0;
m_prManager->GetFilter(&filter);
if (filter != PowerRenameFilters::None)
{
m_prManager->SwitchFilter(0);
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(false);
InvalidateItemListViewState();
}
}
}
@@ -409,14 +421,17 @@ namespace winrt::PowerRenameUI::implementation
button_showRenamed().IsChecked(true);
button_showAll().IsChecked(false);
DWORD filter = 0;
m_prManager->GetFilter(&filter);
if (filter != PowerRenameFilters::ShouldRename)
if (m_prManager)
{
m_prManager->SwitchFilter(0);
UpdateCounts();
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(true);
InvalidateItemListViewState();
DWORD filter = 0;
m_prManager->GetFilter(&filter);
if (filter != PowerRenameFilters::ShouldRename)
{
m_prManager->SwitchFilter(0);
UpdateCounts();
get_self<ExplorerItemsSource>(m_explorerItems)->SetIsFiltered(true);
InvalidateItemListViewState();
}
}
}
@@ -434,6 +449,27 @@ namespace winrt::PowerRenameUI::implementation
textBox_replace().Text(textBox_replace().Text() + s->Code());
}
void MainWindow::MetadataItemClick(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e)
{
auto s = e.ClickedItem().try_as<PatternSnippet>();
DateTimeFlyout().Hide();
textBox_replace().Text(textBox_replace().Text() + s->Code());
}
void MainWindow::MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const&)
{
int selectedIndex = comboBox_metadataSource().SelectedIndex();
// Get the selected metadata type based on ComboBox selection
PowerRenameLib::MetadataType metadataType = static_cast<PowerRenameLib::MetadataType>(selectedIndex);
// Update the metadata shortcuts list
UpdateMetadataShortcuts(metadataType);
// Update the metadata source flags
UpdateMetadataSourceFlags(selectedIndex);
}
void MainWindow::button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const&, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const&)
{
Rename(false);
@@ -621,6 +657,12 @@ namespace winrt::PowerRenameUI::implementation
{
_TRACER_;
if (!m_prManager)
{
// Manager not initialized yet, ignore flag updates
return;
}
DWORD flags{};
m_prManager->GetFlags(&flags);
@@ -818,6 +860,7 @@ namespace winrt::PowerRenameUI::implementation
UpdateFlag(ModificationTime, UpdateFlagCommand::Reset);
}
});
}
void MainWindow::ToggleItem(int32_t id, bool checked)
@@ -1049,6 +1092,15 @@ namespace winrt::PowerRenameUI::implementation
{
toggleButton_capitalize().IsChecked(true);
}
int metadataIndex = (flags & MetadataSourceXMP) ? 1 : 0;
if (comboBox_metadataSource().SelectedIndex() != metadataIndex)
{
comboBox_metadataSource().SelectedIndex(metadataIndex);
}
auto metadataType = metadataIndex == 1 ? PowerRenameLib::MetadataType::XMP : PowerRenameLib::MetadataType::EXIF;
UpdateMetadataShortcuts(metadataType);
}
void MainWindow::UpdateCounts()
@@ -1081,6 +1133,220 @@ namespace winrt::PowerRenameUI::implementation
RenamedCount(hstring{ std::to_wstring(m_renamingCount) });
}
void MainWindow::UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType)
{
// Clear existing list
m_metadataShortcuts.Clear();
// Get supported patterns for the selected metadata type
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
auto factory = winrt::get_activation_factory<ResourceManager, IResourceManagerFactory>();
ResourceManager manager = factory.CreateInstance(L"PowerToys.PowerRename.pri");
// Add each supported pattern to the list
for (const auto& pattern : supportedPatterns)
{
std::wstring resourceKey = L"Resources/MetadataCheatSheet_" + ConvertPatternToResourceKey(pattern);
winrt::hstring patternWithDollar = winrt::hstring(L"$" + pattern);
try {
auto description = manager.MainResourceMap().GetValue(resourceKey).ValueAsString();
m_metadataShortcuts.Append(winrt::make<PatternSnippet>(patternWithDollar, description));
}
catch (...) {
// If resource doesn't exist, use the pattern name as description
m_metadataShortcuts.Append(winrt::make<PatternSnippet>(patternWithDollar, winrt::hstring(pattern)));
}
}
}
std::wstring MainWindow::ConvertPatternToResourceKey(const std::wstring& pattern)
{
// Special cases for patterns that don't follow the standard naming convention
if (pattern == L"TITLE")
{
return L"DocTitle";
}
else if (pattern == L"DATE_TAKEN_YYYY")
{
return L"DateTakenYear4";
}
else if (pattern == L"DATE_TAKEN_YY")
{
return L"DateTakenYear2";
}
else if (pattern == L"DATE_TAKEN_MM")
{
return L"DateTakenMonth";
}
else if (pattern == L"DATE_TAKEN_DD")
{
return L"DateTakenDay";
}
else if (pattern == L"DATE_TAKEN_HH")
{
return L"DateTakenHour";
}
else if (pattern == L"DATE_TAKEN_mm")
{
return L"DateTakenMinute";
}
else if (pattern == L"DATE_TAKEN_SS")
{
return L"DateTakenSecond";
}
else if (pattern == L"CREATE_DATE_YYYY")
{
return L"CreateDateYear4";
}
else if (pattern == L"CREATE_DATE_YY")
{
return L"CreateDateYear2";
}
else if (pattern == L"CREATE_DATE_MM")
{
return L"CreateDateMonth";
}
else if (pattern == L"CREATE_DATE_DD")
{
return L"CreateDateDay";
}
else if (pattern == L"CREATE_DATE_HH")
{
return L"CreateDateHour";
}
else if (pattern == L"CREATE_DATE_mm")
{
return L"CreateDateMinute";
}
else if (pattern == L"CREATE_DATE_SS")
{
return L"CreateDateSecond";
}
else if (pattern == L"MODIFY_DATE_YYYY")
{
return L"ModifyDateYear4";
}
else if (pattern == L"MODIFY_DATE_YY")
{
return L"ModifyDateYear2";
}
else if (pattern == L"MODIFY_DATE_MM")
{
return L"ModifyDateMonth";
}
else if (pattern == L"MODIFY_DATE_DD")
{
return L"ModifyDateDay";
}
else if (pattern == L"MODIFY_DATE_HH")
{
return L"ModifyDateHour";
}
else if (pattern == L"MODIFY_DATE_mm")
{
return L"ModifyDateMinute";
}
else if (pattern == L"MODIFY_DATE_SS")
{
return L"ModifyDateSecond";
}
else if (pattern == L"METADATA_DATE_YYYY")
{
return L"MetadataDateYear4";
}
else if (pattern == L"METADATA_DATE_YY")
{
return L"MetadataDateYear2";
}
else if (pattern == L"METADATA_DATE_MM")
{
return L"MetadataDateMonth";
}
else if (pattern == L"METADATA_DATE_DD")
{
return L"MetadataDateDay";
}
else if (pattern == L"METADATA_DATE_HH")
{
return L"MetadataDateHour";
}
else if (pattern == L"METADATA_DATE_mm")
{
return L"MetadataDateMinute";
}
else if (pattern == L"METADATA_DATE_SS")
{
return L"MetadataDateSecond";
}
else if (pattern == L"ISO")
{
return L"ISO";
}
else if (pattern == L"TITLE")
{
return L"DocTitle";
}
else if (pattern == L"DESCRIPTION")
{
return L"DocDescription";
}
else if (pattern == L"CREATOR")
{
return L"DocCreator";
}
else if (pattern == L"SUBJECT")
{
return L"DocSubject";
}
else if (pattern == L"RIGHTS")
{
return L"Rights";
}
// Convert pattern name to resource key format
// e.g., "CAMERA_MAKE" -> "CameraMake"
std::wstring result;
bool capitalizeNext = true;
for (wchar_t ch : pattern)
{
if (ch == L'_')
{
capitalizeNext = true;
}
else
{
if (capitalizeNext)
{
result += static_cast<wchar_t>(std::toupper(ch));
capitalizeNext = false;
}
else
{
result += static_cast<wchar_t>(std::tolower(ch));
}
}
}
return result;
}
void MainWindow::UpdateMetadataSourceFlags(int selectedIndex)
{
// Clear all metadata source flags first
UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Reset);
UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Reset);
// Set the appropriate metadata source flag based on selection
switch(selectedIndex) {
case 0: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break;
case 1: UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Set); break;
default: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break; // Default to EXIF
}
}
HRESULT MainWindow::OnRename(_In_ IPowerRenameItem* /*renameItem*/)
{
UpdateCounts();
@@ -1122,3 +1388,6 @@ namespace winrt::PowerRenameUI::implementation
return S_OK;
}
}

View File

@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "winrt/Windows.UI.Xaml.h"
#include "winrt/Windows.UI.Xaml.Markup.h"
@@ -20,6 +20,8 @@
#include <PowerRenameManager.h>
#include <PowerRenameInterfaces.h>
#include <PowerRenameMRU.h>
#include <MetadataTypes.h>
#include <MetadataPatternExtractor.h>
namespace winrt::PowerRenameUI::implementation
{
@@ -88,6 +90,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> DateTimeShortcuts() { return m_dateTimeShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> CounterShortcuts() { return m_CounterShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> RandomizerShortcuts() { return m_RandomizerShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> MetadataShortcuts() { return m_metadataShortcuts; }
hstring OriginalCount();
void OriginalCount(hstring value);
@@ -111,6 +114,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_dateTimeShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_CounterShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_RandomizerShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector<PowerRenameUI::PatternSnippet> m_metadataShortcuts;
// Used by PowerRenameManagerEvents
HRESULT OnRename(_In_ IPowerRenameItem* renameItem);
@@ -144,6 +148,9 @@ namespace winrt::PowerRenameUI::implementation
HRESULT OpenSettingsApp();
void SetCheckboxesFromFlags(DWORD flags);
void UpdateCounts();
void UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType);
std::wstring ConvertPatternToResourceKey(const std::wstring& pattern);
void UpdateMetadataSourceFlags(int selectedIndex);
Shared::Trace::ETWTrace m_etwTrace{};
@@ -167,6 +174,8 @@ namespace winrt::PowerRenameUI::implementation
public:
void RegExItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
void DateTimeItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
void MetadataItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e);
void MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& e);
void button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const& sender, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args);
void MenuFlyoutItem_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
void OpenDocs(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
@@ -179,3 +188,4 @@ namespace winrt::PowerRenameUI::factory_implementation
{
};
}

View File

@@ -414,6 +414,9 @@
<data name="TextBlock_FileTime.Text" xml:space="preserve">
<value>Time used for replacement</value>
</data>
<data name="TextBlock_MetadataSource.Text" xml:space="preserve">
<value>Metadata source for replacement</value>
</data>
<data name="FileTimeParts_CreationTime.Content" xml:space="preserve">
<value>Creation Time</value>
</data>
@@ -423,4 +426,149 @@
<data name="FileTimeParts_AccessTime.Content" xml:space="preserve">
<value>Access Time</value>
</data>
<data name="MetadataSource_EXIF.Content" xml:space="preserve">
<value>EXIF Metadata</value>
</data>
<data name="MetadataSource_XMP.Content" xml:space="preserve">
<value>XMP Metadata</value>
</data>
<data name="MetadataCheatSheet_Title.Text" xml:space="preserve">
<value>Replace with media metadata</value>
</data>
<data name="MetadataCheatSheet_CameraMake" xml:space="preserve">
<value>Camera manufacturer name</value>
</data>
<data name="MetadataCheatSheet_CameraModel" xml:space="preserve">
<value>Camera model name</value>
</data>
<data name="MetadataCheatSheet_Lens" xml:space="preserve">
<value>Lens model name</value>
</data>
<data name="MetadataCheatSheet_ISO" xml:space="preserve">
<value>ISO sensitivity value</value>
</data>
<data name="MetadataCheatSheet_Aperture" xml:space="preserve">
<value>F-number aperture value</value>
</data>
<data name="MetadataCheatSheet_Shutter" xml:space="preserve">
<value>Shutter speed value</value>
</data>
<data name="MetadataCheatSheet_Focal" xml:space="preserve">
<value>Focal length in millimeters</value>
</data>
<data name="MetadataCheatSheet_Flash" xml:space="preserve">
<value>Flash status (On/Off)</value>
</data>
<data name="MetadataCheatSheet_Width" xml:space="preserve">
<value>Image width in pixels</value>
</data>
<data name="MetadataCheatSheet_Height" xml:space="preserve">
<value>Image height in pixels</value>
</data>
<data name="MetadataCheatSheet_Author" xml:space="preserve">
<value>Image author/artist</value>
</data>
<data name="MetadataCheatSheet_Copyright" xml:space="preserve">
<value>Copyright information</value>
</data>
<data name="MetadataCheatSheet_Latitude" xml:space="preserve">
<value>GPS latitude coordinate</value>
</data>
<data name="MetadataCheatSheet_Longitude" xml:space="preserve">
<value>GPS longitude coordinate</value>
</data>
<data name="MetadataCheatSheet_Altitude" xml:space="preserve">
<value>GPS altitude in meters</value>
</data>
<data name="MetadataCheatSheet_ExposureBias" xml:space="preserve">
<value>Exposure compensation value</value>
</data>
<data name="MetadataCheatSheet_Orientation" xml:space="preserve">
<value>Image orientation</value>
</data>
<data name="MetadataCheatSheet_ColorSpace" xml:space="preserve">
<value>Color space information</value>
</data>
<data name="MetadataCheatSheet_DateTakenYear4" xml:space="preserve">
<value>Year photo was taken (4 digits)</value>
</data>
<data name="MetadataCheatSheet_DateTakenYear2" xml:space="preserve">
<value>Year photo was taken (2 digits)</value>
</data>
<data name="MetadataCheatSheet_DateTakenMonth" xml:space="preserve">
<value>Month photo was taken (01-12)</value>
</data>
<data name="MetadataCheatSheet_DateTakenDay" xml:space="preserve">
<value>Day photo was taken (01-31)</value>
</data>
<data name="MetadataCheatSheet_DateTakenHour" xml:space="preserve">
<value>Hour photo was taken (00-23)</value>
</data>
<data name="MetadataCheatSheet_DateTakenMinute" xml:space="preserve">
<value>Minute photo was taken (00-59)</value>
</data>
<data name="MetadataCheatSheet_DateTakenSecond" xml:space="preserve">
<value>Second photo was taken (00-59)</value>
</data>
<data name="MetadataCheatSheet_CreateDateYear4" xml:space="preserve">
<value>Year from XMP create date (4 digits)</value>
</data>
<data name="MetadataCheatSheet_CreateDateYear2" xml:space="preserve">
<value>Year from XMP create date (2 digits)</value>
</data>
<data name="MetadataCheatSheet_CreateDateMonth" xml:space="preserve">
<value>Month from XMP create date (01-12)</value>
</data>
<data name="MetadataCheatSheet_CreateDateDay" xml:space="preserve">
<value>Day from XMP create date (01-31)</value>
</data>
<data name="MetadataCheatSheet_CreateDateHour" xml:space="preserve">
<value>Hour from XMP create date (00-23)</value>
</data>
<data name="MetadataCheatSheet_CreateDateMinute" xml:space="preserve">
<value>Minute from XMP create date (00-59)</value>
</data>
<data name="MetadataCheatSheet_CreateDateSecond" xml:space="preserve">
<value>Second from XMP create date (00-59)</value>
</data>
<!-- XMP patterns -->
<data name="MetadataCheatSheet_CreatorTool" xml:space="preserve">
<value>Software used to create/edit</value>
</data>
<!-- Dublin Core patterns -->
<data name="MetadataCheatSheet_DocTitle" xml:space="preserve">
<value>Document title</value>
</data>
<data name="MetadataCheatSheet_DocDescription" xml:space="preserve">
<value>Document description</value>
</data>
<data name="MetadataCheatSheet_DocCreator" xml:space="preserve">
<value>Document creator/author</value>
</data>
<data name="MetadataCheatSheet_DocSubject" xml:space="preserve">
<value>Keywords/tags</value>
</data>
<!-- XMP Rights pattern -->
<data name="MetadataCheatSheet_Rights" xml:space="preserve">
<value>Copyright/rights information</value>
</data>
<!-- XMP Media Management schema patterns -->
<data name="MetadataCheatSheet_DocumentId" xml:space="preserve">
<value>Document unique identifier</value>
</data>
<data name="MetadataCheatSheet_InstanceId" xml:space="preserve">
<value>Instance unique identifier</value>
</data>
<data name="MetadataCheatSheet_OriginalDocumentId" xml:space="preserve">
<value>Original document identifier</value>
</data>
<data name="MetadataCheatSheet_VersionId" xml:space="preserve">
<value>Version identifier</value>
</data>
</root>

View File

@@ -24,7 +24,7 @@
<AdditionalIncludeDirectories>..\lib\;..\PowerRenameUILib\;..\;..\..\..\;..\..\..\common\telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>Pathcch.lib;comctl32.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>Pathcch.lib;comctl32.lib;shcore.lib;windowscodecs.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>PowerRenameExt.def</ModuleDefinitionFile>
<DelayLoadDLLs>gdi32.dll;shell32.dll;ole32.dll;shlwapi.dll;oleaut32.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
</Link>

View File

@@ -1,9 +1,13 @@
#include "pch.h"
#include "Helpers.h"
#include "MetadataTypes.h"
#include <regex>
#include <ShlGuid.h>
#include <cstring>
#include <filesystem>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
namespace fs = std::filesystem;
@@ -12,6 +16,50 @@ namespace
const int MAX_INPUT_STRING_LEN = 1024;
const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\PowerRename";
// Helper function: Find the longest matching pattern starting at the given position
// Returns the matched pattern name, or empty string if no match found
std::wstring FindLongestPattern(
const std::wstring& input,
size_t startPos,
size_t maxPatternLength,
const std::unordered_set<std::wstring>& validPatterns)
{
const size_t remaining = input.length() - startPos;
const size_t searchLength = std::min(maxPatternLength, remaining);
// Try to match from longest to shortest to ensure greedy matching
// e.g., DATE_TAKEN_YYYY should be matched before DATE_TAKEN_YY
for (size_t len = searchLength; len > 0; --len)
{
std::wstring candidate = input.substr(startPos, len);
if (validPatterns.find(candidate) != validPatterns.end())
{
return candidate;
}
}
return L"";
}
// Helper function: Get the replacement value for a pattern
// Returns the actual metadata value if available; if not, returns the pattern name with $ prefix
std::wstring GetPatternValue(
const std::wstring& patternName,
const PowerRenameLib::MetadataPatternMap& patterns)
{
auto it = patterns.find(patternName);
// Return actual value if found and valid (non-empty)
if (it != patterns.end() && !it->second.empty())
{
return it->second;
}
// Return pattern name with $ prefix if value is unavailable
// This provides visual feedback that the field exists but has no data
return L"$" + patternName;
}
}
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source)
@@ -271,6 +319,72 @@ bool isFileTimeUsed(_In_ PCWSTR source)
return used;
}
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath, bool isFolder)
{
if (!source) return false;
// Early exit: If file path is provided, check file type first (fastest checks)
// This avoids expensive pattern matching for files that don't support metadata
if (filePath != nullptr)
{
// Folders don't support metadata extraction
if (isFolder)
{
return false;
}
// Check if file path is valid
if (wcslen(filePath) == 0)
{
return false;
}
// Get file extension
std::wstring extension = fs::path(filePath).extension().wstring();
// Convert to lowercase for case-insensitive comparison
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
// According to the metadata support table, only these formats support metadata extraction:
// - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
// - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
// - PNG (text chunks)
static const std::unordered_set<std::wstring> supportedExtensions = {
L".jpg",
L".jpeg",
L".png",
L".tif",
L".tiff"
};
// If file type doesn't support metadata, no need to check patterns
if (supportedExtensions.find(extension) == supportedExtensions.end())
{
return false;
}
}
// Now check if any metadata pattern exists in the source string
// This is the most expensive check, so we do it last
std::wstring str(source);
// Get supported patterns for the specified metadata type
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
// Check if any metadata pattern exists in the source string
for (const auto& pattern : supportedPatterns)
{
std::wstring searchPattern = L"$" + pattern;
if (str.find(searchPattern) != std::wstring::npos)
{
return true;
}
}
// No metadata pattern found
return false;
}
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
{
std::locale::global(std::locale(""));
@@ -297,10 +411,10 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
@@ -310,13 +424,13 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
@@ -326,19 +440,19 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
formattedDate[0] = towupper(formattedDate[0]);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
@@ -347,31 +461,31 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff
hr = StringCchCopy(result, cchMax, res.c_str());
}
@@ -379,6 +493,91 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
return hr;
}
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns)
{
if (!source || wcslen(source) == 0)
{
return E_INVALIDARG;
}
std::wstring input(source);
std::wstring output;
output.reserve(input.length() * 2); // Reserve space to avoid frequent reallocations
// Build pattern lookup table for fast validation
// Using all possible patterns to recognize valid pattern names even when metadata is unavailable
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
std::unordered_set<std::wstring> validPatterns;
validPatterns.reserve(allPatterns.size());
size_t maxPatternLength = 0;
for (const auto& pattern : allPatterns)
{
validPatterns.insert(pattern);
maxPatternLength = std::max(maxPatternLength, pattern.length());
}
size_t pos = 0;
while (pos < input.length())
{
// Handle regular characters
if (input[pos] != L'$')
{
output += input[pos];
pos++;
continue;
}
// Count consecutive dollar signs
size_t dollarCount = 0;
while (pos < input.length() && input[pos] == L'$')
{
dollarCount++;
pos++;
}
// Even number of dollars: all are escaped (e.g., $$ -> $, $$$$ -> $$)
if (dollarCount % 2 == 0)
{
output.append(dollarCount / 2, L'$');
continue;
}
// Odd number of dollars: pairs are escaped, last one might be a pattern prefix
// e.g., $ -> might be pattern, $$$ -> $ + might be pattern
size_t escapedDollars = dollarCount / 2;
// If no more characters, output all dollar signs
if (pos >= input.length())
{
output.append(dollarCount, L'$');
continue;
}
// Try to match a pattern (greedy matching for longest pattern)
std::wstring matchedPattern = FindLongestPattern(input, pos, maxPatternLength, validPatterns);
if (matchedPattern.empty())
{
// No pattern matched, output all dollar signs
output.append(dollarCount, L'$');
}
else
{
// Pattern matched
output.append(escapedDollars, L'$'); // Output escaped dollars first
// Replace pattern with its value or keep pattern name if value unavailable
std::wstring replacementValue = GetPatternValue(matchedPattern, patterns);
output += replacementValue;
pos += matchedPattern.length();
}
}
return StringCchCopy(result, cchMax, output.c_str());
}
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items)
{
*items = nullptr;
@@ -707,4 +906,4 @@ std::wstring CreateGuidStringWithoutBrackets()
}
return L"";
}
}

View File

@@ -1,13 +1,17 @@
#pragma once
#include "PowerRenameInterfaces.h"
#include "MetadataTypes.h"
#include "MetadataPatternExtractor.h"
#include <string>
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source);
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags, bool isFolder);
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns);
bool isFileTimeUsed(_In_ PCWSTR source);
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath = nullptr, bool isFolder = false);
bool ShellItemArrayContainsRenamableItem(_In_ IShellItemArray* shellItemArray);
bool DataObjectContainsRenamableItem(_In_ IUnknown* dataSource);
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items);

View File

@@ -0,0 +1,237 @@
// 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.
#include "pch.h"
#include "MetadataFormatHelper.h"
#include <format>
#include <cmath>
#include <cstring>
using namespace PowerRenameLib;
// Formatting functions
std::wstring MetadataFormatHelper::FormatAperture(double aperture)
{
return std::format(L"f/{:.1f}", aperture);
}
std::wstring MetadataFormatHelper::FormatShutterSpeed(double speed)
{
if (speed <= 0.0)
{
return L"0";
}
if (speed >= 1.0)
{
return std::format(L"{:.1f}s", speed);
}
const double reciprocal = std::round(1.0 / speed);
if (reciprocal <= 1.0)
{
return std::format(L"{:.3f}s", speed);
}
return std::format(L"1/{:.0f}s", reciprocal);
}
std::wstring MetadataFormatHelper::FormatISO(int64_t iso)
{
if (iso <= 0)
{
return L"ISO";
}
return std::format(L"ISO {}", iso);
}
std::wstring MetadataFormatHelper::FormatFlash(int64_t flashValue)
{
switch (flashValue & 0x1)
{
case 0:
return L"Flash Off";
case 1:
return L"Flash On";
default:
break;
}
return std::format(L"Flash 0x{:X}", static_cast<unsigned int>(flashValue));
}
std::wstring MetadataFormatHelper::FormatCoordinate(double coord, bool isLatitude)
{
wchar_t direction = isLatitude ? (coord >= 0.0 ? L'N' : L'S') : (coord >= 0.0 ? L'E' : L'W');
double absolute = std::abs(coord);
int degrees = static_cast<int>(absolute);
double minutes = (absolute - static_cast<double>(degrees)) * 60.0;
return std::format(L"{:d}°{:.2f}'{}", degrees, minutes, direction);
}
std::wstring MetadataFormatHelper::FormatSystemTime(const SYSTEMTIME& st)
{
return std::format(L"{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}",
st.wYear,
st.wMonth,
st.wDay,
st.wHour,
st.wMinute,
st.wSecond);
}
// Parsing functions
double MetadataFormatHelper::ParseGPSRational(const PROPVARIANT& pv)
{
if ((pv.vt & VT_VECTOR) && pv.caub.cElems >= 8)
{
return ParseSingleRational(pv.caub.pElems, 0);
}
return 0.0;
}
double MetadataFormatHelper::ParseSingleRational(const uint8_t* bytes, size_t offset)
{
// Parse a single rational number (8 bytes: numerator + denominator)
if (!bytes)
return 0.0;
// Note: Callers are responsible for ensuring the buffer is large enough.
// This function assumes offset points to at least 8 bytes of valid data.
// All current callers perform cElems >= required_size checks before calling.
const uint8_t* rationalBytes = bytes + offset;
// Parse as little-endian uint32_t values
uint32_t numerator = static_cast<uint32_t>(rationalBytes[0]) |
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
(static_cast<uint32_t>(rationalBytes[3]) << 24);
uint32_t denominator = static_cast<uint32_t>(rationalBytes[4]) |
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
(static_cast<uint32_t>(rationalBytes[7]) << 24);
if (denominator != 0)
{
return static_cast<double>(numerator) / static_cast<double>(denominator);
}
return 0.0;
}
double MetadataFormatHelper::ParseSingleSRational(const uint8_t* bytes, size_t offset)
{
// Parse a single signed rational number (8 bytes: signed numerator + signed denominator)
if (!bytes)
return 0.0;
// Note: Callers are responsible for ensuring the buffer is large enough.
// This function assumes offset points to at least 8 bytes of valid data.
// All current callers perform cElems >= required_size checks before calling.
const uint8_t* rationalBytes = bytes + offset;
// Parse as little-endian int32_t values (signed)
// First construct as unsigned, then reinterpret as signed
uint32_t numerator_uint = static_cast<uint32_t>(rationalBytes[0]) |
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
(static_cast<uint32_t>(rationalBytes[3]) << 24);
uint32_t denominator_uint = static_cast<uint32_t>(rationalBytes[4]) |
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
(static_cast<uint32_t>(rationalBytes[7]) << 24);
// Reinterpret as signed
int32_t numerator = static_cast<int32_t>(numerator_uint);
int32_t denominator = static_cast<int32_t>(denominator_uint);
if (denominator != 0)
{
return static_cast<double>(numerator) / static_cast<double>(denominator);
}
return 0.0;
}
std::pair<double, double> MetadataFormatHelper::ParseGPSCoordinates(
const PROPVARIANT& latitude,
const PROPVARIANT& longitude,
const PROPVARIANT& latRef,
const PROPVARIANT& lonRef)
{
double lat = 0.0, lon = 0.0;
// Parse latitude - typically stored as 3 rationals (degrees, minutes, seconds)
if ((latitude.vt & VT_VECTOR) && latitude.caub.cElems >= 24) // 3 rationals * 8 bytes each
{
const uint8_t* bytes = latitude.caub.pElems;
// degrees, minutes, seconds (each rational is 8 bytes)
double degrees = ParseSingleRational(bytes, 0);
double minutes = ParseSingleRational(bytes, 8);
double seconds = ParseSingleRational(bytes, 16);
lat = degrees + minutes / 60.0 + seconds / 3600.0;
}
// Parse longitude
if ((longitude.vt & VT_VECTOR) && longitude.caub.cElems >= 24)
{
const uint8_t* bytes = longitude.caub.pElems;
double degrees = ParseSingleRational(bytes, 0);
double minutes = ParseSingleRational(bytes, 8);
double seconds = ParseSingleRational(bytes, 16);
lon = degrees + minutes / 60.0 + seconds / 3600.0;
}
// Apply direction references (N/S for latitude, E/W for longitude)
if (latRef.vt == VT_LPSTR && latRef.pszVal)
{
if (strcmp(latRef.pszVal, "S") == 0)
lat = -lat;
}
if (lonRef.vt == VT_LPSTR && lonRef.pszVal)
{
if (strcmp(lonRef.pszVal, "W") == 0)
lon = -lon;
}
return { lat, lon };
}
std::wstring MetadataFormatHelper::SanitizeForFileName(const std::wstring& str)
{
// Windows illegal filename characters: < > : " / \ | ? *
// Also control characters (0-31) and some others
std::wstring sanitized = str;
// Replace illegal characters with underscore
for (auto& ch : sanitized)
{
// Check for illegal characters
if (ch == L'<' || ch == L'>' || ch == L':' || ch == L'"' ||
ch == L'/' || ch == L'\\' || ch == L'|' || ch == L'?' || ch == L'*' ||
ch < 32) // Control characters
{
ch = L'_';
}
}
// Also remove trailing dots and spaces (Windows doesn't like them at end of filename)
while (!sanitized.empty() && (sanitized.back() == L'.' || sanitized.back() == L' '))
{
sanitized.pop_back();
}
return sanitized;
}

View File

@@ -0,0 +1,117 @@
// 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.
#pragma once
#include <string>
#include <utility>
#include <windows.h>
#include <propvarutil.h>
namespace PowerRenameLib
{
/// <summary>
/// Helper class for formatting and parsing metadata values
/// Provides static utility functions for converting metadata to human-readable strings
/// and parsing raw metadata values
/// </summary>
class MetadataFormatHelper
{
public:
// Formatting functions - Convert metadata values to display strings
/// <summary>
/// Format aperture value (f-number)
/// </summary>
/// <param name="aperture">Aperture value (e.g., 2.8)</param>
/// <returns>Formatted string (e.g., "f/2.8")</returns>
static std::wstring FormatAperture(double aperture);
/// <summary>
/// Format shutter speed
/// </summary>
/// <param name="speed">Shutter speed in seconds</param>
/// <returns>Formatted string (e.g., "1/100s" or "2.5s")</returns>
static std::wstring FormatShutterSpeed(double speed);
/// <summary>
/// Format ISO value
/// </summary>
/// <param name="iso">ISO speed value</param>
/// <returns>Formatted string (e.g., "ISO 400")</returns>
static std::wstring FormatISO(int64_t iso);
/// <summary>
/// Format flash status
/// </summary>
/// <param name="flashValue">Flash value from EXIF</param>
/// <returns>Formatted string (e.g., "Flash On" or "Flash Off")</returns>
static std::wstring FormatFlash(int64_t flashValue);
/// <summary>
/// Format GPS coordinate
/// </summary>
/// <param name="coord">Coordinate value in decimal degrees</param>
/// <param name="isLatitude">true for latitude, false for longitude</param>
/// <returns>Formatted string (e.g., "40°26.76'N")</returns>
static std::wstring FormatCoordinate(double coord, bool isLatitude);
/// <summary>
/// Format SYSTEMTIME to string
/// </summary>
/// <param name="st">SYSTEMTIME structure</param>
/// <returns>Formatted string (e.g., "2024-03-15 14:30:45")</returns>
static std::wstring FormatSystemTime(const SYSTEMTIME& st);
// Parsing functions - Convert raw metadata to usable values
/// <summary>
/// Parse GPS rational value from PROPVARIANT
/// </summary>
/// <param name="pv">PROPVARIANT containing GPS rational data</param>
/// <returns>Parsed double value</returns>
static double ParseGPSRational(const PROPVARIANT& pv);
/// <summary>
/// Parse single rational value from byte array
/// </summary>
/// <param name="bytes">Byte array containing rational data</param>
/// <param name="offset">Offset in the byte array</param>
/// <returns>Parsed double value (numerator / denominator)</returns>
static double ParseSingleRational(const uint8_t* bytes, size_t offset);
/// <summary>
/// Parse single signed rational value from byte array
/// </summary>
/// <param name="bytes">Byte array containing signed rational data</param>
/// <param name="offset">Offset in the byte array</param>
/// <returns>Parsed double value (signed numerator / signed denominator)</returns>
static double ParseSingleSRational(const uint8_t* bytes, size_t offset);
/// <summary>
/// Parse GPS coordinates from PROPVARIANT values
/// </summary>
/// <param name="latitude">PROPVARIANT containing latitude</param>
/// <param name="longitude">PROPVARIANT containing longitude</param>
/// <param name="latRef">PROPVARIANT containing latitude reference (N/S)</param>
/// <param name="lonRef">PROPVARIANT containing longitude reference (E/W)</param>
/// <returns>Pair of (latitude, longitude) in decimal degrees</returns>
static std::pair<double, double> ParseGPSCoordinates(
const PROPVARIANT& latitude,
const PROPVARIANT& longitude,
const PROPVARIANT& latRef,
const PROPVARIANT& lonRef);
/// <summary>
/// Sanitize a string to make it safe for use in filenames
/// Replaces illegal filename characters (< > : " / \ | ? * and control chars) with underscore
/// Also removes trailing dots and spaces which Windows doesn't allow at end of filename
///
/// IMPORTANT: This should ONLY be called in ExtractPatterns to avoid performance waste.
/// Do NOT call this function when reading raw metadata values.
/// </summary>
/// <param name="str">String to sanitize</param>
/// <returns>Sanitized string safe for use in filename</returns>
static std::wstring SanitizeForFileName(const std::wstring& str);
};
}

View File

@@ -0,0 +1,353 @@
// 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.
#include "pch.h"
#include "MetadataPatternExtractor.h"
#include "MetadataFormatHelper.h"
#include "WICMetadataExtractor.h"
#include <algorithm>
#include <format>
#include <sstream>
#include <iomanip>
#include <cmath>
#include <utility>
using namespace PowerRenameLib;
MetadataPatternExtractor::MetadataPatternExtractor()
: extractor(std::make_unique<WICMetadataExtractor>())
{
}
MetadataPatternExtractor::~MetadataPatternExtractor() = default;
MetadataPatternMap MetadataPatternExtractor::ExtractPatterns(
const std::wstring& filePath,
MetadataType type)
{
MetadataPatternMap patterns;
switch (type)
{
case MetadataType::EXIF:
patterns = ExtractEXIFPatterns(filePath);
break;
case MetadataType::XMP:
patterns = ExtractXMPPatterns(filePath);
break;
default:
return MetadataPatternMap();
}
// Sanitize all pattern values for filename safety before returning
// This ensures all metadata values are safe to use in filenames (removes illegal chars like <>:"/\|?*)
// IMPORTANT: Only call SanitizeForFileName here to avoid performance waste
for (auto& [key, value] : patterns)
{
value = MetadataFormatHelper::SanitizeForFileName(value);
}
return patterns;
}
void MetadataPatternExtractor::ClearCache()
{
if (extractor)
{
extractor->ClearCache();
}
}
MetadataPatternMap MetadataPatternExtractor::ExtractEXIFPatterns(const std::wstring& filePath)
{
MetadataPatternMap patterns;
EXIFMetadata exif;
if (!extractor->ExtractEXIFMetadata(filePath, exif))
{
return patterns;
}
if (exif.cameraMake.has_value())
{
patterns[MetadataPatterns::CAMERA_MAKE] = exif.cameraMake.value();
}
if (exif.cameraModel.has_value())
{
patterns[MetadataPatterns::CAMERA_MODEL] = exif.cameraModel.value();
}
if (exif.lensModel.has_value())
{
patterns[MetadataPatterns::LENS] = exif.lensModel.value();
}
if (exif.iso.has_value())
{
patterns[MetadataPatterns::ISO] = MetadataFormatHelper::FormatISO(exif.iso.value());
}
if (exif.aperture.has_value())
{
patterns[MetadataPatterns::APERTURE] = MetadataFormatHelper::FormatAperture(exif.aperture.value());
}
if (exif.shutterSpeed.has_value())
{
patterns[MetadataPatterns::SHUTTER] = MetadataFormatHelper::FormatShutterSpeed(exif.shutterSpeed.value());
}
if (exif.focalLength.has_value())
{
patterns[MetadataPatterns::FOCAL] = std::to_wstring(static_cast<int>(exif.focalLength.value())) + L"mm";
}
if (exif.flash.has_value())
{
patterns[MetadataPatterns::FLASH] = MetadataFormatHelper::FormatFlash(exif.flash.value());
}
if (exif.width.has_value())
{
patterns[MetadataPatterns::WIDTH] = std::to_wstring(exif.width.value());
}
if (exif.height.has_value())
{
patterns[MetadataPatterns::HEIGHT] = std::to_wstring(exif.height.value());
}
if (exif.author.has_value())
{
patterns[MetadataPatterns::AUTHOR] = exif.author.value();
}
if (exif.copyright.has_value())
{
patterns[MetadataPatterns::COPYRIGHT] = exif.copyright.value();
}
if (exif.latitude.has_value())
{
patterns[MetadataPatterns::LATITUDE] = MetadataFormatHelper::FormatCoordinate(exif.latitude.value(), true);
}
if (exif.longitude.has_value())
{
patterns[MetadataPatterns::LONGITUDE] = MetadataFormatHelper::FormatCoordinate(exif.longitude.value(), false);
}
// Only extract DATE_TAKEN patterns (most commonly used)
if (exif.dateTaken.has_value())
{
const SYSTEMTIME& date = exif.dateTaken.value();
patterns[MetadataPatterns::DATE_TAKEN_YYYY] = std::format(L"{:04d}", date.wYear);
patterns[MetadataPatterns::DATE_TAKEN_YY] = std::format(L"{:02d}", date.wYear % 100);
patterns[MetadataPatterns::DATE_TAKEN_MM] = std::format(L"{:02d}", date.wMonth);
patterns[MetadataPatterns::DATE_TAKEN_DD] = std::format(L"{:02d}", date.wDay);
patterns[MetadataPatterns::DATE_TAKEN_HH] = std::format(L"{:02d}", date.wHour);
patterns[MetadataPatterns::DATE_TAKEN_mm] = std::format(L"{:02d}", date.wMinute);
patterns[MetadataPatterns::DATE_TAKEN_SS] = std::format(L"{:02d}", date.wSecond);
}
// Note: dateDigitized and dateModified are still extracted but not exposed as patterns
if (exif.exposureBias.has_value())
{
patterns[MetadataPatterns::EXPOSURE_BIAS] = std::format(L"{:.2f}", exif.exposureBias.value());
}
if (exif.orientation.has_value())
{
patterns[MetadataPatterns::ORIENTATION] = std::to_wstring(exif.orientation.value());
}
if (exif.colorSpace.has_value())
{
patterns[MetadataPatterns::COLOR_SPACE] = std::to_wstring(exif.colorSpace.value());
}
if (exif.altitude.has_value())
{
patterns[MetadataPatterns::ALTITUDE] = std::format(L"{:.2f} m", exif.altitude.value());
}
return patterns;
}
MetadataPatternMap MetadataPatternExtractor::ExtractXMPPatterns(const std::wstring& filePath)
{
MetadataPatternMap patterns;
XMPMetadata xmp;
if (!extractor->ExtractXMPMetadata(filePath, xmp))
{
return patterns;
}
if (xmp.creator.has_value())
{
const auto& creator = xmp.creator.value();
patterns[MetadataPatterns::AUTHOR] = creator;
patterns[MetadataPatterns::CREATOR] = creator;
}
if (xmp.rights.has_value())
{
const auto& rights = xmp.rights.value();
patterns[MetadataPatterns::RIGHTS] = rights;
patterns[MetadataPatterns::COPYRIGHT] = rights;
}
if (xmp.title.has_value())
{
patterns[MetadataPatterns::TITLE] = xmp.title.value();
}
if (xmp.description.has_value())
{
patterns[MetadataPatterns::DESCRIPTION] = xmp.description.value();
}
if (xmp.subject.has_value())
{
std::wstring joined;
for (const auto& entry : xmp.subject.value())
{
if (!joined.empty())
{
joined.append(L"; ");
}
joined.append(entry);
}
if (!joined.empty())
{
patterns[MetadataPatterns::SUBJECT] = joined;
}
}
if (xmp.creatorTool.has_value())
{
patterns[MetadataPatterns::CREATOR_TOOL] = xmp.creatorTool.value();
}
if (xmp.documentID.has_value())
{
patterns[MetadataPatterns::DOCUMENT_ID] = xmp.documentID.value();
}
if (xmp.instanceID.has_value())
{
patterns[MetadataPatterns::INSTANCE_ID] = xmp.instanceID.value();
}
if (xmp.originalDocumentID.has_value())
{
patterns[MetadataPatterns::ORIGINAL_DOCUMENT_ID] = xmp.originalDocumentID.value();
}
if (xmp.versionID.has_value())
{
patterns[MetadataPatterns::VERSION_ID] = xmp.versionID.value();
}
// Only extract CREATE_DATE patterns (primary creation time)
if (xmp.createDate.has_value())
{
const SYSTEMTIME& date = xmp.createDate.value();
patterns[MetadataPatterns::CREATE_DATE_YYYY] = std::format(L"{:04d}", date.wYear);
patterns[MetadataPatterns::CREATE_DATE_YY] = std::format(L"{:02d}", date.wYear % 100);
patterns[MetadataPatterns::CREATE_DATE_MM] = std::format(L"{:02d}", date.wMonth);
patterns[MetadataPatterns::CREATE_DATE_DD] = std::format(L"{:02d}", date.wDay);
patterns[MetadataPatterns::CREATE_DATE_HH] = std::format(L"{:02d}", date.wHour);
patterns[MetadataPatterns::CREATE_DATE_mm] = std::format(L"{:02d}", date.wMinute);
patterns[MetadataPatterns::CREATE_DATE_SS] = std::format(L"{:02d}", date.wSecond);
}
// Note: modifyDate and metadataDate are still extracted but not exposed as patterns
return patterns;
}
// AddDatePatterns function has been removed as dynamic patterns are no longer supported.
// Date patterns are now directly added inline for DATE_TAKEN and CREATE_DATE only.
// Formatting functions have been moved to MetadataFormatHelper for better testability.
std::vector<std::wstring> MetadataPatternExtractor::GetSupportedPatterns(MetadataType type)
{
switch (type)
{
case MetadataType::EXIF:
return {
MetadataPatterns::CAMERA_MAKE,
MetadataPatterns::CAMERA_MODEL,
MetadataPatterns::LENS,
MetadataPatterns::ISO,
MetadataPatterns::APERTURE,
MetadataPatterns::SHUTTER,
MetadataPatterns::FOCAL,
MetadataPatterns::FLASH,
MetadataPatterns::WIDTH,
MetadataPatterns::HEIGHT,
MetadataPatterns::AUTHOR,
MetadataPatterns::COPYRIGHT,
MetadataPatterns::LATITUDE,
MetadataPatterns::LONGITUDE,
MetadataPatterns::DATE_TAKEN_YYYY,
MetadataPatterns::DATE_TAKEN_YY,
MetadataPatterns::DATE_TAKEN_MM,
MetadataPatterns::DATE_TAKEN_DD,
MetadataPatterns::DATE_TAKEN_HH,
MetadataPatterns::DATE_TAKEN_mm,
MetadataPatterns::DATE_TAKEN_SS,
MetadataPatterns::EXPOSURE_BIAS,
MetadataPatterns::ORIENTATION,
MetadataPatterns::COLOR_SPACE,
MetadataPatterns::ALTITUDE
};
case MetadataType::XMP:
return {
MetadataPatterns::AUTHOR,
MetadataPatterns::COPYRIGHT,
MetadataPatterns::RIGHTS,
MetadataPatterns::TITLE,
MetadataPatterns::DESCRIPTION,
MetadataPatterns::SUBJECT,
MetadataPatterns::CREATOR,
MetadataPatterns::CREATOR_TOOL,
MetadataPatterns::DOCUMENT_ID,
MetadataPatterns::INSTANCE_ID,
MetadataPatterns::ORIGINAL_DOCUMENT_ID,
MetadataPatterns::VERSION_ID,
MetadataPatterns::CREATE_DATE_YYYY,
MetadataPatterns::CREATE_DATE_YY,
MetadataPatterns::CREATE_DATE_MM,
MetadataPatterns::CREATE_DATE_DD,
MetadataPatterns::CREATE_DATE_HH,
MetadataPatterns::CREATE_DATE_mm,
MetadataPatterns::CREATE_DATE_SS
};
default:
return {};
}
}
std::vector<std::wstring> MetadataPatternExtractor::GetAllPossiblePatterns()
{
auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF);
auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP);
std::vector<std::wstring> allPatterns;
allPatterns.reserve(exifPatterns.size() + xmpPatterns.size());
allPatterns.insert(allPatterns.end(), exifPatterns.begin(), exifPatterns.end());
allPatterns.insert(allPatterns.end(), xmpPatterns.begin(), xmpPatterns.end());
std::sort(allPatterns.begin(), allPatterns.end());
allPatterns.erase(std::unique(allPatterns.begin(), allPatterns.end()), allPatterns.end());
return allPatterns;
}

View File

@@ -0,0 +1,39 @@
// 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.
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include "MetadataTypes.h"
namespace PowerRenameLib
{
// Pattern-Value mapping for metadata replacement
using MetadataPatternMap = std::unordered_map<std::wstring, std::wstring>;
/// <summary>
/// Metadata pattern extractor that converts metadata into replaceable patterns
/// </summary>
class MetadataPatternExtractor
{
public:
MetadataPatternExtractor();
~MetadataPatternExtractor();
MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type);
void ClearCache();
static std::vector<std::wstring> GetSupportedPatterns(MetadataType type);
static std::vector<std::wstring> GetAllPossiblePatterns();
private:
std::unique_ptr<class WICMetadataExtractor> extractor;
MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
};
}

View File

@@ -0,0 +1,87 @@
// 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.
#include "pch.h"
#include "MetadataResultCache.h"
using namespace PowerRenameLib;
namespace
{
template <typename Metadata, typename CacheEntry, typename Cache, typename Mutex, typename Loader>
bool GetOrLoadInternal(const std::wstring& filePath,
Metadata& outMetadata,
Cache& cache,
Mutex& mutex,
const Loader& loader)
{
{
std::shared_lock sharedLock(mutex);
auto it = cache.find(filePath);
if (it != cache.end())
{
// Return cached result (success or failure)
outMetadata = it->second.data;
return it->second.wasSuccessful;
}
}
if (!loader)
{
// No loader provided
return false;
}
Metadata loaded{};
const bool result = loader(loaded);
// Cache the result (success or failure)
{
std::unique_lock uniqueLock(mutex);
// Check if another thread cached it while we were loading
auto it = cache.find(filePath);
if (it == cache.end())
{
// Not cached yet, insert our result
cache.emplace(filePath, CacheEntry{ result, loaded });
}
else
{
// Another thread cached it, use their result
outMetadata = it->second.data;
return it->second.wasSuccessful;
}
}
outMetadata = loaded;
return result;
}
}
bool MetadataResultCache::GetOrLoadEXIF(const std::wstring& filePath,
EXIFMetadata& outMetadata,
const EXIFLoader& loader)
{
return GetOrLoadInternal<EXIFMetadata, CacheEntry<EXIFMetadata>>(filePath, outMetadata, exifCache, exifMutex, loader);
}
bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath,
XMPMetadata& outMetadata,
const XMPLoader& loader)
{
return GetOrLoadInternal<XMPMetadata, CacheEntry<XMPMetadata>>(filePath, outMetadata, xmpCache, xmpMutex, loader);
}
void MetadataResultCache::ClearAll()
{
{
std::unique_lock lock(exifMutex);
exifCache.clear();
}
{
std::unique_lock lock(xmpMutex);
xmpCache.clear();
}
}

View File

@@ -0,0 +1,39 @@
// 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.
#pragma once
#include "MetadataTypes.h"
#include <shared_mutex>
#include <unordered_map>
#include <string>
#include <functional>
namespace PowerRenameLib
{
class MetadataResultCache
{
public:
using EXIFLoader = std::function<bool(EXIFMetadata&)>;
using XMPLoader = std::function<bool(XMPMetadata&)>;
bool GetOrLoadEXIF(const std::wstring& filePath, EXIFMetadata& outMetadata, const EXIFLoader& loader);
bool GetOrLoadXMP(const std::wstring& filePath, XMPMetadata& outMetadata, const XMPLoader& loader);
void ClearAll();
private:
// Wrapper to cache both success and failure states
template<typename T>
struct CacheEntry
{
bool wasSuccessful;
T data;
};
mutable std::shared_mutex exifMutex;
mutable std::shared_mutex xmpMutex;
std::unordered_map<std::wstring, CacheEntry<EXIFMetadata>> exifCache;
std::unordered_map<std::wstring, CacheEntry<XMPMetadata>> xmpCache;
};
}

View File

@@ -0,0 +1,156 @@
// 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.
#pragma once
#include <string>
#include <optional>
#include <vector>
#include <windows.h>
namespace PowerRenameLib
{
/// <summary>
/// Supported metadata format types
/// </summary>
enum class MetadataType
{
EXIF, // EXIF metadata (camera settings, date taken, etc.)
XMP // XMP metadata (Dublin Core, Photoshop, etc.)
};
/// <summary>
/// Complete EXIF metadata structure
/// Contains all commonly used EXIF fields with optional values
/// </summary>
struct EXIFMetadata
{
// Date and time information
std::optional<SYSTEMTIME> dateTaken; // DateTimeOriginal
std::optional<SYSTEMTIME> dateDigitized; // DateTimeDigitized
std::optional<SYSTEMTIME> dateModified; // DateTime
// Camera information
std::optional<std::wstring> cameraMake; // Make
std::optional<std::wstring> cameraModel; // Model
std::optional<std::wstring> lensModel; // LensModel
// Shooting parameters
std::optional<int64_t> iso; // ISO speed
std::optional<double> aperture; // F-number
std::optional<double> shutterSpeed; // Exposure time
std::optional<double> focalLength; // Focal length in mm
std::optional<double> exposureBias; // Exposure bias value
std::optional<int64_t> flash; // Flash status
// Image properties
std::optional<int64_t> width; // Image width in pixels
std::optional<int64_t> height; // Image height in pixels
std::optional<int64_t> orientation; // Image orientation
std::optional<int64_t> colorSpace; // Color space
// Author and copyright
std::optional<std::wstring> author; // Artist
std::optional<std::wstring> copyright; // Copyright notice
// GPS information
std::optional<double> latitude; // GPS latitude in decimal degrees
std::optional<double> longitude; // GPS longitude in decimal degrees
std::optional<double> altitude; // GPS altitude in meters
};
/// <summary>
/// XMP (Extensible Metadata Platform) metadata structure
/// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields
/// </summary>
struct XMPMetadata
{
// XMP Basic schema - https://ns.adobe.com/xap/1.0/
std::optional<SYSTEMTIME> createDate; // xmp:CreateDate
std::optional<SYSTEMTIME> modifyDate; // xmp:ModifyDate
std::optional<SYSTEMTIME> metadataDate; // xmp:MetadataDate
std::optional<std::wstring> creatorTool; // xmp:CreatorTool
// Dublin Core schema - http://purl.org/dc/elements/1.1/
std::optional<std::wstring> title; // dc:title
std::optional<std::wstring> description; // dc:description
std::optional<std::wstring> creator; // dc:creator (author)
std::optional<std::vector<std::wstring>> subject; // dc:subject (keywords)
// XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/
std::optional<std::wstring> rights; // xmpRights:WebStatement (copyright)
// XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/
std::optional<std::wstring> documentID; // xmpMM:DocumentID
std::optional<std::wstring> instanceID; // xmpMM:InstanceID
std::optional<std::wstring> originalDocumentID; // xmpMM:OriginalDocumentID
std::optional<std::wstring> versionID; // xmpMM:VersionID
};
/// <summary>
/// Constants for metadata pattern names
/// </summary>
namespace MetadataPatterns
{
// EXIF patterns
constexpr wchar_t CAMERA_MAKE[] = L"CAMERA_MAKE";
constexpr wchar_t CAMERA_MODEL[] = L"CAMERA_MODEL";
constexpr wchar_t LENS[] = L"LENS";
constexpr wchar_t ISO[] = L"ISO";
constexpr wchar_t APERTURE[] = L"APERTURE";
constexpr wchar_t SHUTTER[] = L"SHUTTER";
constexpr wchar_t FOCAL[] = L"FOCAL";
constexpr wchar_t FLASH[] = L"FLASH";
constexpr wchar_t WIDTH[] = L"WIDTH";
constexpr wchar_t HEIGHT[] = L"HEIGHT";
constexpr wchar_t AUTHOR[] = L"AUTHOR";
constexpr wchar_t COPYRIGHT[] = L"COPYRIGHT";
constexpr wchar_t LATITUDE[] = L"LATITUDE";
constexpr wchar_t LONGITUDE[] = L"LONGITUDE";
// Date components from EXIF DateTimeOriginal (when photo was taken)
constexpr wchar_t DATE_TAKEN_YYYY[] = L"DATE_TAKEN_YYYY";
constexpr wchar_t DATE_TAKEN_YY[] = L"DATE_TAKEN_YY";
constexpr wchar_t DATE_TAKEN_MM[] = L"DATE_TAKEN_MM";
constexpr wchar_t DATE_TAKEN_DD[] = L"DATE_TAKEN_DD";
constexpr wchar_t DATE_TAKEN_HH[] = L"DATE_TAKEN_HH";
constexpr wchar_t DATE_TAKEN_mm[] = L"DATE_TAKEN_mm";
constexpr wchar_t DATE_TAKEN_SS[] = L"DATE_TAKEN_SS";
// Additional EXIF patterns
constexpr wchar_t EXPOSURE_BIAS[] = L"EXPOSURE_BIAS";
constexpr wchar_t ORIENTATION[] = L"ORIENTATION";
constexpr wchar_t COLOR_SPACE[] = L"COLOR_SPACE";
constexpr wchar_t ALTITUDE[] = L"ALTITUDE";
// XMP patterns
constexpr wchar_t CREATOR_TOOL[] = L"CREATOR_TOOL";
// Date components from XMP CreateDate
constexpr wchar_t CREATE_DATE_YYYY[] = L"CREATE_DATE_YYYY";
constexpr wchar_t CREATE_DATE_YY[] = L"CREATE_DATE_YY";
constexpr wchar_t CREATE_DATE_MM[] = L"CREATE_DATE_MM";
constexpr wchar_t CREATE_DATE_DD[] = L"CREATE_DATE_DD";
constexpr wchar_t CREATE_DATE_HH[] = L"CREATE_DATE_HH";
constexpr wchar_t CREATE_DATE_mm[] = L"CREATE_DATE_mm";
constexpr wchar_t CREATE_DATE_SS[] = L"CREATE_DATE_SS";
// Dublin Core patterns
constexpr wchar_t TITLE[] = L"TITLE";
constexpr wchar_t DESCRIPTION[] = L"DESCRIPTION";
constexpr wchar_t CREATOR[] = L"CREATOR";
constexpr wchar_t SUBJECT[] = L"SUBJECT"; // Keywords
// XMP Rights pattern
constexpr wchar_t RIGHTS[] = L"RIGHTS"; // Copyright
// XMP Media Management patterns
constexpr wchar_t DOCUMENT_ID[] = L"DOCUMENT_ID";
constexpr wchar_t INSTANCE_ID[] = L"INSTANCE_ID";
constexpr wchar_t ORIGINAL_DOCUMENT_ID[] = L"ORIGINAL_DOCUMENT_ID";
constexpr wchar_t VERSION_ID[] = L"VERSION_ID";
}
}

View File

@@ -1,7 +1,10 @@
#pragma once
#include "pch.h"
#include "MetadataTypes.h"
#include "MetadataPatternExtractor.h"
#include <string>
#include <vector>
#include <unordered_map>
enum PowerRenameFlags
{
@@ -22,6 +25,9 @@ enum PowerRenameFlags
CreationTime = 0x4000,
ModificationTime = 0x8000,
AccessTime = 0x10000,
// Metadata source flags
MetadataSourceEXIF = 0x20000, // Default
MetadataSourceXMP = 0x40000,
};
enum PowerRenameFilters
@@ -47,6 +53,7 @@ public:
IFACEMETHOD(OnReplaceTermChanged)(_In_ PCWSTR replaceTerm) = 0;
IFACEMETHOD(OnFlagsChanged)(_In_ DWORD flags) = 0;
IFACEMETHOD(OnFileTimeChanged)(_In_ SYSTEMTIME fileTime) = 0;
IFACEMETHOD(OnMetadataChanged)() = 0;
};
interface __declspec(uuid("E3ED45B5-9CE0-47E2-A595-67EB950B9B72")) IPowerRenameRegEx : public IUnknown
@@ -62,6 +69,9 @@ public:
IFACEMETHOD(PutFlags)(_In_ DWORD flags) = 0;
IFACEMETHOD(PutFileTime)(_In_ SYSTEMTIME fileTime) = 0;
IFACEMETHOD(ResetFileTime)() = 0;
IFACEMETHOD(PutMetadataPatterns)(_In_ const PowerRenameLib::MetadataPatternMap& patterns) = 0;
IFACEMETHOD(ResetMetadata)() = 0;
IFACEMETHOD(GetMetadataType)(_Out_ PowerRenameLib::MetadataType* metadataType) = 0;
IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex) = 0;
};

View File

@@ -74,7 +74,7 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
else
{
// Default to modification time if no specific flag is set
parsedTimeType = PowerRenameFlags::CreationTime;
parsedTimeType = PowerRenameFlags::CreationTime;
}
if (m_isTimeParsed && parsedTimeType == m_parsedTimeType)
@@ -86,6 +86,13 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
HANDLE hFile = CreateFileW(m_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
// Use RAII-style scope guard to ensure handle is always closed
struct FileHandleCloser
{
HANDLE handle;
~FileHandleCloser() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); }
} scopedHandle{ hFile };
FILETIME FileTime;
bool success = false;
@@ -122,8 +129,6 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
}
}
}
CloseHandle(hFile);
}
*time = m_time;
return hr;

View File

@@ -16,19 +16,24 @@
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutDir>
<DepsPath>$(ProjectDir)..\..\..\..\deps</DepsPath>
</PropertyGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<PreprocessorDefinitions>WIN32;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ProjectDir)..\;$(ProjectDir)..\ui;$(ProjectDir)..\dll;$(ProjectDir)..\lib;$(ProjectDir)..\..\..\;$(ProjectDir)..\..\..\common\Telemetry;%(AdditionalIncludeDirectories);$(GeneratedFilesDir)</AdditionalIncludeDirectories>
<AdditionalOptions>/FS %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Enumerating.h" />
@@ -47,6 +52,12 @@
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="MetadataTypes.h" />
<ClInclude Include="PropVariantValue.h" />
<ClInclude Include="WICMetadataExtractor.h" />
<ClInclude Include="MetadataPatternExtractor.h" />
<ClInclude Include="MetadataFormatHelper.h" />
<ClInclude Include="MetadataResultCache.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Enumerating.cpp" />
@@ -64,6 +75,10 @@
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
<ClCompile Include="WICMetadataExtractor.cpp" />
<ClCompile Include="MetadataPatternExtractor.cpp" />
<ClCompile Include="MetadataFormatHelper.cpp" />
<ClCompile Include="MetadataResultCache.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -462,6 +462,12 @@ IFACEMETHODIMP CPowerRenameManager::OnFileTimeChanged(_In_ SYSTEMTIME /*fileTime
return S_OK;
}
IFACEMETHODIMP CPowerRenameManager::OnMetadataChanged()
{
_PerformRegExRename();
return S_OK;
}
HRESULT CPowerRenameManager::s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm)
{
*ppsrm = nullptr;

View File

@@ -50,6 +50,7 @@ public:
IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm);
IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags);
IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime);
IFACEMETHODIMP OnMetadataChanged();
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm);

View File

@@ -328,6 +328,22 @@ IFACEMETHODIMP CPowerRenameRegEx::ResetFileTime()
return S_OK;
}
IFACEMETHODIMP CPowerRenameRegEx::PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns)
{
m_metadataPatterns = patterns;
m_useMetadata = true;
_OnMetadataChanged();
return S_OK;
}
IFACEMETHODIMP CPowerRenameRegEx::ResetMetadata()
{
m_metadataPatterns.clear();
m_useMetadata = false;
_OnMetadataChanged();
return S_OK;
}
HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx)
{
*renameRegEx = nullptr;
@@ -387,10 +403,39 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
// TODO: creating the regex could be costly. May want to cache this.
wchar_t newReplaceTerm[MAX_PATH] = { 0 };
bool fileTimeErrorOccurred = false;
bool metadataErrorOccurred = false;
bool appliedTemplateTransform = false;
std::wstring replaceTemplate;
if (m_replaceTerm)
{
replaceTemplate = m_replaceTerm;
}
if (m_useFileTime)
{
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_fileTime)))
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_fileTime)))
{
fileTimeErrorOccurred = true;
}
else
{
replaceTemplate.assign(newReplaceTerm);
appliedTemplateTransform = true;
}
}
if (m_useMetadata)
{
if (FAILED(GetMetadataFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_metadataPatterns)))
{
metadataErrorOccurred = true;
}
else
{
replaceTemplate.assign(newReplaceTerm);
appliedTemplateTransform = true;
}
}
std::wstring sourceToUse;
@@ -399,9 +444,9 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
std::wstring searchTerm(m_searchTerm);
std::wstring replaceTerm;
if (m_useFileTime && !fileTimeErrorOccurred)
if (appliedTemplateTransform)
{
replaceTerm = newReplaceTerm;
replaceTerm = replaceTemplate;
}
else if (m_replaceTerm)
{
@@ -606,3 +651,43 @@ void CPowerRenameRegEx::_OnFileTimeChanged()
}
}
}
void CPowerRenameRegEx::_OnMetadataChanged()
{
CSRWSharedAutoLock lock(&m_lockEvents);
for (auto it : m_renameRegExEvents)
{
if (it.pEvents)
{
it.pEvents->OnMetadataChanged();
}
}
}
PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() const
{
if (m_flags & MetadataSourceXMP)
return PowerRenameLib::MetadataType::XMP;
// Default to EXIF
return PowerRenameLib::MetadataType::EXIF;
}
// Interface method implementation
IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType)
{
if (metadataType == nullptr)
return E_POINTER;
*metadataType = _GetMetadataTypeFromFlags();
return S_OK;
}
// Convenience method for internal use
PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
{
return _GetMetadataTypeFromFlags();
}

View File

@@ -5,6 +5,8 @@
#include "Enumerating.h"
#include "Randomizer.h"
#include "MetadataTypes.h"
#include "MetadataPatternExtractor.h"
#include "PowerRenameInterfaces.h"
@@ -29,7 +31,13 @@ public:
IFACEMETHODIMP PutFlags(_In_ DWORD flags);
IFACEMETHODIMP PutFileTime(_In_ SYSTEMTIME fileTime);
IFACEMETHODIMP ResetFileTime();
IFACEMETHODIMP PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns);
IFACEMETHODIMP ResetMetadata();
IFACEMETHODIMP GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType);
IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex);
// Get current metadata type based on flags
PowerRenameLib::MetadataType GetMetadataType() const;
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx);
@@ -41,7 +49,9 @@ protected:
void _OnReplaceTermChanged();
void _OnFlagsChanged();
void _OnFileTimeChanged();
void _OnMetadataChanged();
HRESULT _OnEnumerateOrRandomizeItemsChanged();
PowerRenameLib::MetadataType _GetMetadataTypeFromFlags() const;
size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos);
@@ -54,6 +64,9 @@ protected:
SYSTEMTIME m_fileTime = { 0 };
bool m_useFileTime = false;
PowerRenameLib::MetadataPatternMap m_metadataPatterns;
bool m_useMetadata = false;
CSRWLock m_lock;
CSRWLock m_lockEvents;

View File

@@ -0,0 +1,62 @@
#pragma once
#include <propvarutil.h>
#include <propidl.h>
namespace PowerRenameLib
{
/// <summary>
/// RAII wrapper around PROPVARIANT to ensure proper initialization and cleanup.
/// Move-only semantics keep ownership simple while still allowing use in optionals.
/// </summary>
struct PropVariantValue
{
PropVariantValue() noexcept
{
PropVariantInit(&value);
}
~PropVariantValue()
{
PropVariantClear(&value);
}
PropVariantValue(const PropVariantValue&) = delete;
PropVariantValue& operator=(const PropVariantValue&) = delete;
PropVariantValue(PropVariantValue&& other) noexcept
{
value = other.value;
PropVariantInit(&other.value); // Properly clear the moved-from object
}
PropVariantValue& operator=(PropVariantValue&& other) noexcept
{
if (this != &other)
{
PropVariantClear(&value);
value = other.value;
PropVariantInit(&other.value); // Properly clear the moved-from object
}
return *this;
}
PROPVARIANT* GetAddressOf() noexcept
{
return &value;
}
PROPVARIANT& Get() noexcept
{
return value;
}
const PROPVARIANT& Get() const noexcept
{
return value;
}
private:
PROPVARIANT value;
};
}

View File

@@ -1,9 +1,13 @@
#include "pch.h"
#include <winrt/base.h>
#include <memory>
#include <mutex>
#include <optional>
#include "Renaming.h"
#include <Helpers.h>
#include "MetadataPatternExtractor.h"
#include "PowerRenameRegEx.h"
namespace fs = std::filesystem;
bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnumIndex, CComPtr<IPowerRenameItem>& spItem)
@@ -14,6 +18,7 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
PWSTR replaceTerm = nullptr;
bool useFileTime = false;
bool useMetadata = false;
winrt::check_hresult(spRenameRegEx->GetReplaceTerm(&replaceTerm));
@@ -21,7 +26,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
{
useFileTime = true;
}
CoTaskMemFree(replaceTerm);
int id = -1;
winrt::check_hresult(spItem->GetId(&id));
@@ -30,6 +34,29 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
bool isSubFolderContent = false;
winrt::check_hresult(spItem->GetIsFolder(&isFolder));
winrt::check_hresult(spItem->GetIsSubFolderContent(&isSubFolderContent));
// Get metadata type to check if metadata patterns are used
PowerRenameLib::MetadataType metadataType;
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
if (FAILED(hr))
{
// Fallback to default metadata type if call fails
metadataType = PowerRenameLib::MetadataType::EXIF;
}
// Check if metadata is used AND if this file type supports metadata
// Get file path early for metadata type checking and reuse later
PWSTR filePath = nullptr;
winrt::check_hresult(spItem->GetPath(&filePath));
std::wstring filePathStr(filePath); // Copy once for reuse
CoTaskMemFree(filePath); // Free immediately after copying
if (isMetadataUsed(replaceTerm, metadataType, filePathStr.c_str(), isFolder))
{
useMetadata = true;
}
CoTaskMemFree(replaceTerm);
if ((isFolder && (flags & PowerRenameFlags::ExcludeFolders)) ||
(!isFolder && (flags & PowerRenameFlags::ExcludeFiles)) ||
(isSubFolderContent && (flags & PowerRenameFlags::ExcludeSubfolders)) ||
@@ -82,6 +109,53 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
winrt::check_hresult(spRenameRegEx->PutFileTime(fileTime));
}
if (useMetadata)
{
// Extract metadata patterns from the file
// Note: filePathStr was already obtained and saved earlier for reuse
// Get metadata type using the interface method
PowerRenameLib::MetadataType metadataType;
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
if (FAILED(hr))
{
// Fallback to default metadata type if call fails
metadataType = PowerRenameLib::MetadataType::EXIF;
}
// Extract all patterns for the selected metadata type
// At this point we know the file is a supported image format (jpg/jpeg/png/tif/tiff)
static std::mutex s_metadataMutex; // Mutex to protect static variables
static std::once_flag s_metadataExtractorInitFlag;
static std::shared_ptr<PowerRenameLib::MetadataPatternExtractor> s_metadataExtractor;
static std::optional<PowerRenameLib::MetadataType> s_activeMetadataType;
// Initialize the extractor only once
std::call_once(s_metadataExtractorInitFlag, []() {
s_metadataExtractor = std::make_shared<PowerRenameLib::MetadataPatternExtractor>();
});
// Protect access to shared state
{
std::lock_guard<std::mutex> lock(s_metadataMutex);
// Clear cache if metadata type has changed
if (s_activeMetadataType.has_value() && s_activeMetadataType.value() != metadataType)
{
s_metadataExtractor->ClearCache();
}
// Update the active metadata type
s_activeMetadataType = metadataType;
}
// Extract patterns (this can be done outside the lock if ExtractPatterns is thread-safe)
PowerRenameLib::MetadataPatternMap patterns = s_metadataExtractor->ExtractPatterns(filePathStr, metadataType);
// Always call PutMetadataPatterns to ensure all patterns get replaced
// Even if empty, this keeps metadata placeholders consistent when no values are extracted
winrt::check_hresult(spRenameRegEx->PutMetadataPatterns(patterns));
}
PWSTR newName = nullptr;
// Failure here means we didn't match anything or had nothing to match
@@ -93,6 +167,10 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
winrt::check_hresult(spRenameRegEx->ResetFileTime());
}
if (useMetadata)
{
winrt::check_hresult(spRenameRegEx->ResetMetadata());
}
wchar_t resultName[MAX_PATH] = { 0 };
PWSTR newNameToUse = nullptr;
@@ -206,4 +284,4 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
CoTaskMemFree(originalName);
return wouldRename;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
// 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.
#pragma once
#include "MetadataTypes.h"
#include "MetadataResultCache.h"
#include "PropVariantValue.h"
#include <wincodec.h>
#include <atlbase.h>
namespace PowerRenameLib
{
/// <summary>
/// Windows Imaging Component (WIC) implementation for metadata extraction
/// Provides efficient batch extraction of all metadata types with built-in caching
/// </summary>
class WICMetadataExtractor
{
public:
WICMetadataExtractor();
~WICMetadataExtractor();
// Public metadata extraction methods
bool ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata);
bool ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata);
void ClearCache();
private:
// WIC factory management
static CComPtr<IWICImagingFactory> GetWICFactory();
static void InitializeWIC();
// WIC operations
CComPtr<IWICBitmapDecoder> CreateDecoder(const std::wstring& filePath);
CComPtr<IWICMetadataQueryReader> GetMetadataReader(IWICBitmapDecoder* decoder);
bool LoadEXIFMetadata(const std::wstring& filePath, EXIFMetadata& outMetadata);
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
// Batch extraction methods
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
// Field reading helpers
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<int64_t> ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<double> ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path);
// Helper methods
std::optional<PropVariantValue> ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path);
private:
MetadataResultCache cache;
};
}

View File

@@ -28,5 +28,17 @@
#include <charconv>
#include <string>
#include <random>
#include <map>
#include <memory>
#include <fstream>
#include <chrono>
#include <mutex>
#include <unordered_map>
#include <winrt/base.h>
// Windows Imaging Component (WIC) headers
#include <wincodec.h>
#include <wincodecsdk.h>
#include <propkey.h>
#include <propvarutil.h>

View File

@@ -0,0 +1,766 @@
#include "pch.h"
#include "Helpers.h"
#include "MetadataPatternExtractor.h"
#include "MetadataTypes.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace HelpersTests
{
TEST_CLASS(GetMetadataFileNameTests)
{
public:
TEST_METHOD(BasicPatternReplacement)
{
// Test basic pattern replacement with available metadata
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
patterns[L"ISO"] = L"ISO 400";
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_Canon_ISO 400", result);
}
TEST_METHOD(PatternWithoutValueShowsPatternName)
{
// Test that patterns without values show the pattern name with $ prefix
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
// ISO is not in the map
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_Canon_$ISO", result);
}
TEST_METHOD(EmptyPatternShowsPatternName)
{
// Test that patterns with empty value show the pattern name with $ prefix
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
patterns[L"ISO"] = L"";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_Canon_$ISO", result);
}
TEST_METHOD(EscapedDollarSigns)
{
// Test that $$ is converted to single $
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"ISO"] = L"ISO 400";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$_$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_$_ISO 400", result);
}
TEST_METHOD(MultipleEscapedDollarSigns)
{
// Test that $$$$ is converted to $$
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"ISO"] = L"ISO 400";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$$price", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_$$price", result);
}
TEST_METHOD(OddDollarSignsWithPattern)
{
// Test that $$$ becomes $ followed by pattern
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"ISO"] = L"ISO 400";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_$ISO 400", result);
}
TEST_METHOD(LongestPatternMatchPriority)
{
// Test that longer patterns are matched first (DATE_TAKEN_YYYY vs DATE_TAKEN_YY)
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
patterns[L"DATE_TAKEN_YY"] = L"24";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$DATE_TAKEN_YYYY", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_2024", result);
}
TEST_METHOD(MultiplePatterns)
{
// Test multiple patterns in one string
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
patterns[L"CAMERA_MODEL"] = L"EOS R5";
patterns[L"ISO"] = L"ISO 800";
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"$DATE_TAKEN_YYYY-$CAMERA_MAKE-$CAMERA_MODEL-$ISO", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"2024-Canon-EOS R5-ISO 800", result);
}
TEST_METHOD(UnrecognizedPatternIgnored)
{
// Test that unrecognized patterns are not replaced
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$INVALID_PATTERN", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_Canon_$INVALID_PATTERN", result);
}
TEST_METHOD(NoPatterns)
{
// Test string with no patterns
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_name_without_patterns", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_name_without_patterns", result);
}
TEST_METHOD(EmptyInput)
{
// Test with empty input string
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"", patterns);
Assert::IsTrue(FAILED(hr));
}
TEST_METHOD(NullInput)
{
// Test with null input
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, nullptr, patterns);
Assert::IsTrue(FAILED(hr));
}
TEST_METHOD(DollarAtEnd)
{
// Test dollar sign at the end of string
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"ISO"] = L"ISO 400";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO$", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_ISO 400$", result);
}
TEST_METHOD(ThreeDollarsAtEnd)
{
// Test three dollar signs at the end
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo$$$", result);
}
TEST_METHOD(ComplexMixedScenario)
{
// Test complex scenario with mixed patterns, escapes, and regular text
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
patterns[L"ISO"] = L"ISO 400";
patterns[L"APERTURE"] = L"f/2.8";
patterns[L"LENS"] = L""; // Empty value
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"$$price_$CAMERA_MAKE_$$$ISO_$APERTURE_$LENS_$$end", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"$price_Canon_$ISO 400_f/2.8_$LENS_$end", result);
}
TEST_METHOD(AllEXIFPatterns)
{
// Test with various EXIF patterns
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"WIDTH"] = L"4000";
patterns[L"HEIGHT"] = L"3000";
patterns[L"FOCAL"] = L"50mm";
patterns[L"SHUTTER"] = L"1/100s";
patterns[L"FLASH"] = L"Flash Off";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"photo_$WIDTH x $HEIGHT_$FOCAL_$SHUTTER_$FLASH", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_4000 x 3000_50mm_1/100s_Flash Off", result);
}
TEST_METHOD(AllXMPPatterns)
{
// Test with various XMP patterns
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"TITLE"] = L"Sunset";
patterns[L"CREATOR"] = L"John Doe";
patterns[L"DESCRIPTION"] = L"Beautiful sunset";
patterns[L"CREATE_DATE_YYYY"] = L"2024";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"$CREATE_DATE_YYYY-$TITLE-by-$CREATOR", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"2024-Sunset-by-John Doe", result);
}
TEST_METHOD(DateComponentPatterns)
{
// Test date component patterns
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
patterns[L"DATE_TAKEN_MM"] = L"03";
patterns[L"DATE_TAKEN_DD"] = L"15";
patterns[L"DATE_TAKEN_HH"] = L"14";
patterns[L"DATE_TAKEN_mm"] = L"30";
patterns[L"DATE_TAKEN_SS"] = L"45";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"photo_$DATE_TAKEN_YYYY-$DATE_TAKEN_MM-$DATE_TAKEN_DD_$DATE_TAKEN_HH-$DATE_TAKEN_mm-$DATE_TAKEN_SS",
patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_2024-03-15_14-30-45", result);
}
TEST_METHOD(SpecialCharactersInValues)
{
// Test that special characters in metadata values are preserved
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"TITLE"] = L"Photo (with) [brackets] & symbols!";
patterns[L"DESCRIPTION"] = L"Test: value; with, punctuation.";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH,
L"$TITLE - $DESCRIPTION", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"Photo (with) [brackets] & symbols! - Test: value; with, punctuation.", result);
}
TEST_METHOD(ConsecutivePatternsWithoutSeparator)
{
// Test consecutive patterns without separator
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
patterns[L"CAMERA_MODEL"] = L"R5";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE$CAMERA_MODEL", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"CanonR5", result);
}
TEST_METHOD(PatternAtStart)
{
// Test pattern at the beginning of string
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE_photo", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"Canon_photo", result);
}
TEST_METHOD(PatternAtEnd)
{
// Test pattern at the end of string
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo_Canon", result);
}
TEST_METHOD(OnlyPattern)
{
// Test string with only a pattern
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"Canon", result);
}
};
TEST_CLASS(PatternMatchingTests)
{
public:
TEST_METHOD(VerifyLongestPatternMatching)
{
// This test verifies the greedy matching behavior
// When we have overlapping pattern names, the longest should be matched first
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"DATE_TAKEN_Y"] = L"4";
patterns[L"DATE_TAKEN_YY"] = L"24";
patterns[L"DATE_TAKEN_YYYY"] = L"2024";
wchar_t result[MAX_PATH] = { 0 };
// Should match YYYY (longest)
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YYYY", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"2024", result);
// Should match YY (available pattern)
hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YY", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"24", result);
}
TEST_METHOD(PartialPatternNames)
{
// Test that partial pattern names don't match longer patterns
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MODEL"] = L"EOS R5";
wchar_t result[MAX_PATH] = { 0 };
// CAMERA is not a valid pattern, should not match
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MODEL", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"EOS R5", result);
}
TEST_METHOD(CaseSensitivePatterns)
{
// Test that pattern names are case-sensitive
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
wchar_t result[MAX_PATH] = { 0 };
// lowercase should not match
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$camera_make", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"$camera_make", result); // Not replaced
}
TEST_METHOD(EmptyPatternMap)
{
// Test with empty pattern map
PowerRenameLib::MetadataPatternMap patterns; // Empty
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO_$CAMERA_MAKE", patterns);
Assert::IsTrue(SUCCEEDED(hr));
// Patterns should show with $ prefix since they're valid but have no values
Assert::AreEqual(L"photo_$ISO_$CAMERA_MAKE", result);
}
};
TEST_CLASS(EdgeCaseTests)
{
public:
TEST_METHOD(VeryLongString)
{
// Test with a very long input string
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"CAMERA_MAKE"] = L"Canon";
std::wstring longInput = L"prefix_";
for (int i = 0; i < 100; i++)
{
longInput += L"$CAMERA_MAKE_";
}
wchar_t result[4096] = { 0 };
HRESULT hr = GetMetadataFileName(result, 4096, longInput.c_str(), patterns);
Assert::IsTrue(SUCCEEDED(hr));
// Verify it starts correctly
Assert::IsTrue(wcsstr(result, L"prefix_Canon_") == result);
}
TEST_METHOD(ManyConsecutiveDollars)
{
// Test with many consecutive dollar signs
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
// 8 dollars should become 4 dollars
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$$$$$$name", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"photo$$$$name", result);
}
TEST_METHOD(OnlyDollars)
{
// Test string with only dollar signs
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$$$$", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"$$", result);
}
TEST_METHOD(UnicodeCharacters)
{
// Test with unicode characters in pattern values
PowerRenameLib::MetadataPatternMap patterns;
patterns[L"TITLE"] = L"照片_фото_φωτογραφία";
patterns[L"CREATOR"] = L"张三_Иван_Γιάννης";
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$TITLE-$CREATOR", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"照片_фото_φωτογραφία-张三_Иван_Γιάννης", result);
}
TEST_METHOD(SingleDollar)
{
// Test with single dollar not followed by pattern
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"price$100", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"price$100", result);
}
TEST_METHOD(DollarFollowedByNumber)
{
// Test dollar followed by numbers (not a pattern)
PowerRenameLib::MetadataPatternMap patterns;
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"cost_$123.45", patterns);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"cost_$123.45", result);
}
};
TEST_CLASS(GetDatedFileNameTests)
{
public:
// Helper to get a fixed test time for consistent testing
SYSTEMTIME GetTestTime()
{
SYSTEMTIME testTime = { 0 };
testTime.wYear = 2024;
testTime.wMonth = 3; // March
testTime.wDay = 15; // 15th
testTime.wHour = 14; // 2 PM (24-hour format)
testTime.wMinute = 30;
testTime.wSecond = 45;
testTime.wMilliseconds = 123;
testTime.wDayOfWeek = 5; // Friday (0=Sunday, 5=Friday)
return testTime;
}
// Category 1: Tests for invalid patterns with extra characters (verify negative lookahead prevents wrong matching)
TEST_METHOD(InvalidPattern_YYY_NotMatched)
{
// Test $YYY (3 Y's) is not a valid pattern and should remain unchanged
// Negative lookahead in $YY(?!Y) prevents matching $YYY
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYY", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_$YYY", result); // $YYY is invalid, should remain unchanged
}
TEST_METHOD(InvalidPattern_DDD_NotPartiallyMatched)
{
// Test that $DDD (short weekday) is not confused with $DD (2-digit day)
// This verifies negative lookahead works correctly
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Fri", result); // Should be "Fri", not "15D"
}
TEST_METHOD(InvalidPattern_MMM_NotPartiallyMatched)
{
// Test that $MMM (short month name) is not confused with $MM (2-digit month)
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "03M"
}
TEST_METHOD(InvalidPattern_HHH_NotMatched)
{
// Test $HHH (3 H's) is not valid and negative lookahead prevents $HH from matching
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HHH", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_$HHH", result); // Should remain unchanged
}
TEST_METHOD(SeparatedPatterns_SingleY)
{
// Test multiple $Y with separators works correctly
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y-$Y-$Y", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_4-4-4", result); // Each $Y outputs "4" (from 2024)
}
TEST_METHOD(SeparatedPatterns_SingleD)
{
// Test multiple $D with separators works correctly
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$D.$D.$D", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_15.15.15", result); // Each $D outputs "15"
}
// Category 2: Tests for mixed length patterns (verify longer patterns don't get matched incorrectly)
TEST_METHOD(MixedLengthYear_QuadFollowedBySingle)
{
// Test $YYYY$Y - should be 2024 + 4
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY$Y", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_20244", result);
}
TEST_METHOD(MixedLengthDay_TripleFollowedBySingle)
{
// Test $DDD$D - should be "Fri" + "15"
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD$D", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Fri15", result);
}
TEST_METHOD(MixedLengthDay_QuadFollowedByDouble)
{
// Test $DDDD$DD - should be "Friday" + "15"
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD$DD", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Friday15", result);
}
TEST_METHOD(MixedLengthMonth_TripleFollowedBySingle)
{
// Test $MMM$M - should be "Mar" + "3"
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM$M", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Mar3", result);
}
// Category 3: Tests for boundary conditions (patterns at start, end, with special chars)
TEST_METHOD(PatternAtStart)
{
// Test pattern at the very start of filename
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY$M$D", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"2024315", result);
}
TEST_METHOD(PatternAtEnd)
{
// Test pattern at the very end of filename
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_4", result);
}
TEST_METHOD(PatternWithSpecialChars)
{
// Test patterns surrounded by special characters
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file-$Y.$Y-$M", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file-4.4-3", result);
}
TEST_METHOD(EmptyFileName)
{
// Test with empty input string - should return E_INVALIDARG
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"", testTime);
Assert::IsTrue(FAILED(hr)); // Empty string should fail
Assert::AreEqual(E_INVALIDARG, hr);
}
// Category 4: Tests to explicitly verify negative lookahead is working
TEST_METHOD(NegativeLookahead_YearNotMatchedInYYYY)
{
// Verify $Y doesn't match when part of $YYYY
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_2024", result); // Should be "2024", not "202Y"
}
TEST_METHOD(NegativeLookahead_MonthNotMatchedInMMM)
{
// Verify $M doesn't match when part of $MMM
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "3ar"
}
TEST_METHOD(NegativeLookahead_DayNotMatchedInDDDD)
{
// Verify $D doesn't match when part of $DDDD
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_Friday", result); // Should be "Friday", not "15riday"
}
TEST_METHOD(NegativeLookahead_HourNotMatchedInHH)
{
// Verify $H doesn't match when part of $HH
// Note: $HH is 12-hour format, so 14:00 (2 PM) displays as "02"
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HH", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_02", result); // 14:00 in 12-hour format is "02 PM"
}
TEST_METHOD(NegativeLookahead_MillisecondNotMatchedInFFF)
{
// Verify $f doesn't match when part of $fff
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$fff", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"file_123", result); // Should be "123", not "1ff"
}
// Category 5: Complex mixed scenarios
TEST_METHOD(ComplexMixedPattern_AllFormats)
{
// Test a complex realistic filename with mixed pattern lengths
// Note: Using $hh for 24-hour format instead of $HH (which is 12-hour)
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"Photo_$YYYY-$MM-$DD_$hh-$mm-$ss_$fff", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"Photo_2024-03-15_14-30-45_123", result);
}
TEST_METHOD(ComplexMixedPattern_WithSeparators)
{
// Test multiple patterns of different lengths with separators
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY_$Y-$Y_$MM_$M", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"2024_4-4_03_3", result);
}
TEST_METHOD(ComplexMixedPattern_DayFormats)
{
// Test all day format variations in one string
SYSTEMTIME testTime = GetTestTime();
wchar_t result[MAX_PATH] = { 0 };
HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$D-$DD-$DDD-$DDDD", testTime);
Assert::IsTrue(SUCCEEDED(hr));
Assert::AreEqual(L"15-15-Fri-Friday", result);
}
};
}

View File

@@ -0,0 +1,487 @@
#include "pch.h"
#include "MetadataFormatHelper.h"
#include <cmath>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace PowerRenameLib;
namespace MetadataFormatHelperTests
{
TEST_CLASS(FormatApertureTests)
{
public:
TEST_METHOD(FormatAperture_ValidValue)
{
// Test formatting a typical aperture value
std::wstring result = MetadataFormatHelper::FormatAperture(2.8);
Assert::AreEqual(L"f/2.8", result.c_str());
}
TEST_METHOD(FormatAperture_SmallValue)
{
// Test small aperture (large f-number)
std::wstring result = MetadataFormatHelper::FormatAperture(1.4);
Assert::AreEqual(L"f/1.4", result.c_str());
}
TEST_METHOD(FormatAperture_LargeValue)
{
// Test large aperture (small f-number)
std::wstring result = MetadataFormatHelper::FormatAperture(22.0);
Assert::AreEqual(L"f/22.0", result.c_str());
}
TEST_METHOD(FormatAperture_RoundedValue)
{
// Test rounding to one decimal place
std::wstring result = MetadataFormatHelper::FormatAperture(5.66666);
Assert::AreEqual(L"f/5.7", result.c_str());
}
TEST_METHOD(FormatAperture_Zero)
{
// Test zero value
std::wstring result = MetadataFormatHelper::FormatAperture(0.0);
Assert::AreEqual(L"f/0.0", result.c_str());
}
};
TEST_CLASS(FormatShutterSpeedTests)
{
public:
TEST_METHOD(FormatShutterSpeed_FastSpeed)
{
// Test fast shutter speed (fraction of second)
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.002);
Assert::AreEqual(L"1/500s", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_VeryFastSpeed)
{
// Test very fast shutter speed
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0001);
Assert::AreEqual(L"1/10000s", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_SlowSpeed)
{
// Test slow shutter speed (more than 1 second)
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(2.5);
Assert::AreEqual(L"2.5s", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_OneSecond)
{
// Test exactly 1 second
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(1.0);
Assert::AreEqual(L"1.0s", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_VerySlowSpeed)
{
// Test very slow shutter speed (< 1 second but close)
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.5);
Assert::AreEqual(L"1/2s", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_Zero)
{
// Test zero value
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0);
Assert::AreEqual(L"0", result.c_str());
}
TEST_METHOD(FormatShutterSpeed_Negative)
{
// Test negative value (invalid but should handle gracefully)
std::wstring result = MetadataFormatHelper::FormatShutterSpeed(-1.0);
Assert::AreEqual(L"0", result.c_str());
}
};
TEST_CLASS(FormatISOTests)
{
public:
TEST_METHOD(FormatISO_TypicalValue)
{
// Test typical ISO value
std::wstring result = MetadataFormatHelper::FormatISO(400);
Assert::AreEqual(L"ISO 400", result.c_str());
}
TEST_METHOD(FormatISO_LowValue)
{
// Test low ISO value
std::wstring result = MetadataFormatHelper::FormatISO(100);
Assert::AreEqual(L"ISO 100", result.c_str());
}
TEST_METHOD(FormatISO_HighValue)
{
// Test high ISO value
std::wstring result = MetadataFormatHelper::FormatISO(12800);
Assert::AreEqual(L"ISO 12800", result.c_str());
}
TEST_METHOD(FormatISO_Zero)
{
// Test zero value
std::wstring result = MetadataFormatHelper::FormatISO(0);
Assert::AreEqual(L"ISO", result.c_str());
}
TEST_METHOD(FormatISO_Negative)
{
// Test negative value (invalid but should handle gracefully)
std::wstring result = MetadataFormatHelper::FormatISO(-100);
Assert::AreEqual(L"ISO", result.c_str());
}
};
TEST_CLASS(FormatFlashTests)
{
public:
TEST_METHOD(FormatFlash_Off)
{
// Test flash off (bit 0 = 0)
std::wstring result = MetadataFormatHelper::FormatFlash(0x0);
Assert::AreEqual(L"Flash Off", result.c_str());
}
TEST_METHOD(FormatFlash_On)
{
// Test flash on (bit 0 = 1)
std::wstring result = MetadataFormatHelper::FormatFlash(0x1);
Assert::AreEqual(L"Flash On", result.c_str());
}
TEST_METHOD(FormatFlash_OnWithAdditionalFlags)
{
// Test flash on with additional flags
std::wstring result = MetadataFormatHelper::FormatFlash(0x5); // 0b0101 = fired, return detected
Assert::AreEqual(L"Flash On", result.c_str());
}
TEST_METHOD(FormatFlash_OffWithAdditionalFlags)
{
// Test flash off with additional flags
std::wstring result = MetadataFormatHelper::FormatFlash(0x10); // Bit 0 is 0
Assert::AreEqual(L"Flash Off", result.c_str());
}
};
TEST_CLASS(FormatCoordinateTests)
{
public:
TEST_METHOD(FormatCoordinate_NorthLatitude)
{
// Test north latitude
std::wstring result = MetadataFormatHelper::FormatCoordinate(40.7128, true);
Assert::AreEqual(L"40°42.77'N", result.c_str());
}
TEST_METHOD(FormatCoordinate_SouthLatitude)
{
// Test south latitude
std::wstring result = MetadataFormatHelper::FormatCoordinate(-33.8688, true);
Assert::AreEqual(L"33°52.13'S", result.c_str());
}
TEST_METHOD(FormatCoordinate_EastLongitude)
{
// Test east longitude
std::wstring result = MetadataFormatHelper::FormatCoordinate(151.2093, false);
Assert::AreEqual(L"151°12.56'E", result.c_str());
}
TEST_METHOD(FormatCoordinate_WestLongitude)
{
// Test west longitude
std::wstring result = MetadataFormatHelper::FormatCoordinate(-74.0060, false);
Assert::AreEqual(L"74°0.36'W", result.c_str());
}
TEST_METHOD(FormatCoordinate_ZeroLatitude)
{
// Test equator (0 degrees latitude)
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, true);
Assert::AreEqual(L"0°0.00'N", result.c_str());
}
TEST_METHOD(FormatCoordinate_ZeroLongitude)
{
// Test prime meridian (0 degrees longitude)
std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, false);
Assert::AreEqual(L"0°0.00'E", result.c_str());
}
};
TEST_CLASS(FormatSystemTimeTests)
{
public:
TEST_METHOD(FormatSystemTime_ValidDateTime)
{
// Test formatting a valid date and time
SYSTEMTIME st = { 0 };
st.wYear = 2024;
st.wMonth = 3;
st.wDay = 15;
st.wHour = 14;
st.wMinute = 30;
st.wSecond = 45;
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
Assert::AreEqual(L"2024-03-15 14:30:45", result.c_str());
}
TEST_METHOD(FormatSystemTime_Midnight)
{
// Test midnight time
SYSTEMTIME st = { 0 };
st.wYear = 2024;
st.wMonth = 1;
st.wDay = 1;
st.wHour = 0;
st.wMinute = 0;
st.wSecond = 0;
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
Assert::AreEqual(L"2024-01-01 00:00:00", result.c_str());
}
TEST_METHOD(FormatSystemTime_EndOfDay)
{
// Test end of day time
SYSTEMTIME st = { 0 };
st.wYear = 2024;
st.wMonth = 12;
st.wDay = 31;
st.wHour = 23;
st.wMinute = 59;
st.wSecond = 59;
std::wstring result = MetadataFormatHelper::FormatSystemTime(st);
Assert::AreEqual(L"2024-12-31 23:59:59", result.c_str());
}
};
TEST_CLASS(ParseSingleRationalTests)
{
public:
TEST_METHOD(ParseSingleRational_ValidValue)
{
// Test parsing a valid rational: 5/2 = 2.5
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
Assert::AreEqual(2.5, result, 0.001);
}
TEST_METHOD(ParseSingleRational_IntegerResult)
{
// Test parsing rational that results in integer: 10/5 = 2.0
uint8_t bytes[] = { 10, 0, 0, 0, 5, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
Assert::AreEqual(2.0, result, 0.001);
}
TEST_METHOD(ParseSingleRational_LargeNumerator)
{
// Test parsing with large numerator: 1000/100 = 10.0
uint8_t bytes[] = { 0xE8, 0x03, 0, 0, 100, 0, 0, 0 }; // 1000 in little-endian
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
Assert::AreEqual(10.0, result, 0.001);
}
TEST_METHOD(ParseSingleRational_ZeroDenominator)
{
// Test parsing with zero denominator (should return 0.0)
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
Assert::AreEqual(0.0, result, 0.001);
}
TEST_METHOD(ParseSingleRational_ZeroNumerator)
{
// Test parsing with zero numerator: 0/5 = 0.0
uint8_t bytes[] = { 0, 0, 0, 0, 5, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleRational(bytes, 0);
Assert::AreEqual(0.0, result, 0.001);
}
TEST_METHOD(ParseSingleRational_WithOffset)
{
// Test parsing with offset
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 10, 0, 0, 0, 5, 0, 0, 0 }; // Offset = 4
double result = MetadataFormatHelper::ParseSingleRational(bytes, 4);
Assert::AreEqual(2.0, result, 0.001);
}
TEST_METHOD(ParseSingleRational_NullPointer)
{
// Test with null pointer (should return 0.0)
double result = MetadataFormatHelper::ParseSingleRational(nullptr, 0);
Assert::AreEqual(0.0, result, 0.001);
}
};
TEST_CLASS(ParseSingleSRationalTests)
{
public:
TEST_METHOD(ParseSingleSRational_PositiveValue)
{
// Test parsing positive signed rational: 5/2 = 2.5
uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(2.5, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_NegativeNumerator)
{
// Test parsing negative numerator: -5/2 = -2.5
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 2, 0, 0, 0 }; // -5 in two's complement
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(-2.5, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_NegativeDenominator)
{
// Test parsing negative denominator: 5/-2 = -2.5
uint8_t bytes[] = { 5, 0, 0, 0, 0xFE, 0xFF, 0xFF, 0xFF }; // -2 in two's complement
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(-2.5, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_BothNegative)
{
// Test parsing both negative: -5/-2 = 2.5
uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF };
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(2.5, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_ExposureBias)
{
// Test typical exposure bias value: -1/3 ≈ -0.333
uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 3, 0, 0, 0 }; // -1/3
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(-0.333, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_ZeroDenominator)
{
// Test with zero denominator (should return 0.0)
uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 };
double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0);
Assert::AreEqual(0.0, result, 0.001);
}
TEST_METHOD(ParseSingleSRational_NullPointer)
{
// Test with null pointer (should return 0.0)
double result = MetadataFormatHelper::ParseSingleSRational(nullptr, 0);
Assert::AreEqual(0.0, result, 0.001);
}
};
TEST_CLASS(SanitizeForFileNameTests)
{
public:
TEST_METHOD(SanitizeForFileName_ValidString)
{
// Test string without illegal characters
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Canon EOS 5D");
Assert::AreEqual(L"Canon EOS 5D", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithColon)
{
// Test string with colon (illegal character)
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo:001");
Assert::AreEqual(L"Photo_001", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithSlashes)
{
// Test string with forward and backward slashes
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photos/2024\\January");
Assert::AreEqual(L"Photos_2024_January", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithMultipleIllegalChars)
{
// Test string with multiple illegal characters
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<Test>:File|Name*?.txt");
Assert::AreEqual(L"_Test__File_Name__.txt", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithQuotes)
{
// Test string with quotes
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo \"Best Shot\"");
Assert::AreEqual(L"Photo _Best Shot_", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithTrailingDot)
{
// Test string with trailing dot (should be removed)
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename.");
Assert::AreEqual(L"filename", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithTrailingSpace)
{
// Test string with trailing space (should be removed)
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename ");
Assert::AreEqual(L"filename", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithMultipleTrailingDotsAndSpaces)
{
// Test string with multiple trailing dots and spaces
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename. . ");
Assert::AreEqual(L"filename", result.c_str());
}
TEST_METHOD(SanitizeForFileName_WithControlCharacters)
{
// Test string with control characters
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"File\x01Name\x1F");
Assert::AreEqual(L"File_Name_", result.c_str());
}
TEST_METHOD(SanitizeForFileName_EmptyString)
{
// Test empty string
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"");
Assert::AreEqual(L"", result.c_str());
}
TEST_METHOD(SanitizeForFileName_OnlyIllegalCharacters)
{
// Test string with only illegal characters
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<>:\"/\\|?*");
Assert::AreEqual(L"_________", result.c_str());
}
TEST_METHOD(SanitizeForFileName_OnlyTrailingCharacters)
{
// Test string with only dots and spaces (should return empty)
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L". . ");
Assert::AreEqual(L"", result.c_str());
}
TEST_METHOD(SanitizeForFileName_UnicodeCharacters)
{
// Test string with valid Unicode characters
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"照片_2024年");
Assert::AreEqual(L"照片_2024年", result.c_str());
}
TEST_METHOD(SanitizeForFileName_MixedContent)
{
// Test realistic metadata string with multiple issues
std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Copyright © 2024: John/Jane Doe. ");
Assert::AreEqual(L"Copyright © 2024_ John_Jane Doe", result.c_str());
}
};
}

View File

@@ -62,6 +62,12 @@ IFACEMETHODIMP CMockPowerRenameRegExEvents::OnFileTimeChanged(_In_ SYSTEMTIME fi
return S_OK;
}
IFACEMETHODIMP CMockPowerRenameRegExEvents::OnMetadataChanged()
{
return S_OK;
}
HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree)
{
*ppsrree = nullptr;
@@ -74,3 +80,4 @@ HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegEx
}
return hr;
}

View File

@@ -19,6 +19,7 @@ public:
IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm);
IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags);
IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime);
IFACEMETHODIMP OnMetadataChanged();
static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree);
@@ -39,3 +40,4 @@ public:
SYSTEMTIME m_fileTime = { 0 };
long m_refCount;
};

View File

@@ -34,7 +34,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;windowscodecs.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
@@ -49,11 +49,14 @@
<ClInclude Include="CommonRegExTests.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="HelpersTests.cpp" />
<ClCompile Include="MockPowerRenameItem.cpp" />
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
<ClCompile Include="PowerRenameRegExBoostTests.cpp" />
<ClCompile Include="PowerRenameManagerTests.cpp" />
<ClCompile Include="MetadataFormatHelperTests.cpp" />
<ClCompile Include="WICMetadataExtractorTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
@@ -73,8 +76,30 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<!-- Include all test data files for deployment -->
<None Include="testdata\exif_test.jpg">
<DeploymentContent>true</DeploymentContent>
</None>
<None Include="testdata\exif_test_2.jpg">
<DeploymentContent>true</DeploymentContent>
</None>
<None Include="testdata\xmp_test.jpg">
<DeploymentContent>true</DeploymentContent>
</None>
<None Include="testdata\xmp_test_2.jpg">
<DeploymentContent>true</DeploymentContent>
</None>
<None Include="testdata\ATTRIBUTION.md">
<DeploymentContent>true</DeploymentContent>
</None>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Target Name="CopyTestData" AfterTargets="Build">
<ItemGroup>
<TestDataFiles Include="$(ProjectDir)testdata\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(TestDataFiles)" DestinationFolder="$(OutDir)testdata\%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\boost.1.87.0\build\boost.targets" Condition="Exists('..\..\..\..\packages\boost.1.87.0\build\boost.targets')" />

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="HelpersTests.cpp" />
<ClCompile Include="MockPowerRenameItem.cpp" />
<ClCompile Include="MockPowerRenameManagerEvents.cpp" />
<ClCompile Include="MockPowerRenameRegExEvents.cpp" />
@@ -30,6 +31,9 @@
<Filter Include="Header Files">
<UniqueIdentifier>{d34a343a-52ef-4296-83c9-a94fa62062ff}</UniqueIdentifier>
</Filter>
<Filter Include="testdata">
<UniqueIdentifier>{8c9f3e2d-1a4b-4e5f-9c7d-2b8a6f3e1d4c}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerRenameUnitTests.rc">
@@ -38,5 +42,20 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="testdata\exif_test.jpg">
<Filter>testdata</Filter>
</None>
<None Include="testdata\exif_test_2.jpg">
<Filter>testdata</Filter>
</None>
<None Include="testdata\xmp_test.jpg">
<Filter>testdata</Filter>
</None>
<None Include="testdata\xmp_test_2.jpg">
<Filter>testdata</Filter>
</None>
<None Include="testdata\ATTRIBUTION.md">
<Filter>testdata</Filter>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,244 @@
#include "pch.h"
#include "WICMetadataExtractor.h"
#include <filesystem>
#include <sstream>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace PowerRenameLib;
namespace WICMetadataExtractorTests
{
// Helper function to get the test data directory path
std::wstring GetTestDataPath()
{
// Get the directory where the test DLL is located
// When running with vstest, we need to get the DLL module handle
HMODULE hModule = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&GetTestDataPath),
&hModule);
wchar_t modulePath[MAX_PATH];
GetModuleFileNameW(hModule, modulePath, MAX_PATH);
std::filesystem::path dllPath(modulePath);
// Navigate to the test data directory
// The test data is in the output directory alongside the DLL
std::filesystem::path testDataPath = dllPath.parent_path() / L"testdata";
return testDataPath.wstring();
}
TEST_CLASS(ExtractEXIFMetadataTests)
{
public:
TEST_METHOD(ExtractEXIF_InvalidFile_ReturnsFalse)
{
// Test that EXIF extraction fails for nonexistent file
WICMetadataExtractor extractor;
EXIFMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
Assert::IsFalse(result, L"EXIF extraction should fail for nonexistent file");
}
TEST_METHOD(ExtractEXIF_ExifTest_AllFields)
{
// Test exif_test.jpg which contains comprehensive EXIF data
WICMetadataExtractor extractor;
EXIFMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
Assert::IsTrue(result, L"EXIF extraction should succeed");
// Verify all the fields that are in exif_test.jpg
Assert::IsTrue(metadata.cameraMake.has_value(), L"Camera make should be present");
Assert::AreEqual(L"samsung", metadata.cameraMake.value().c_str(), L"Camera make should be samsung");
Assert::IsTrue(metadata.cameraModel.has_value(), L"Camera model should be present");
Assert::AreEqual(L"SM-G930P", metadata.cameraModel.value().c_str(), L"Camera model should be SM-G930P");
Assert::IsTrue(metadata.lensModel.has_value(), L"Lens model should be present");
Assert::AreEqual(L"Samsung Galaxy S7 Rear Camera", metadata.lensModel.value().c_str(), L"Lens model should match");
Assert::IsTrue(metadata.iso.has_value(), L"ISO should be present");
Assert::AreEqual(40, static_cast<int>(metadata.iso.value()), L"ISO should be 40");
Assert::IsTrue(metadata.aperture.has_value(), L"Aperture should be present");
Assert::AreEqual(1.7, metadata.aperture.value(), 0.01, L"Aperture should be f/1.7");
Assert::IsTrue(metadata.shutterSpeed.has_value(), L"Shutter speed should be present");
Assert::AreEqual(0.000625, metadata.shutterSpeed.value(), 0.000001, L"Shutter speed should be 0.000625s");
Assert::IsTrue(metadata.focalLength.has_value(), L"Focal length should be present");
Assert::AreEqual(4.2, metadata.focalLength.value(), 0.1, L"Focal length should be 4.2mm");
Assert::IsTrue(metadata.flash.has_value(), L"Flash should be present");
Assert::AreEqual(0u, static_cast<unsigned int>(metadata.flash.value()), L"Flash should be 0x0");
Assert::IsTrue(metadata.exposureBias.has_value(), L"Exposure bias should be present");
Assert::AreEqual(0.0, metadata.exposureBias.value(), 0.01, L"Exposure bias should be 0 EV");
Assert::IsTrue(metadata.author.has_value(), L"Author should be present");
Assert::AreEqual(L"Carl Seibert (Exif)", metadata.author.value().c_str(), L"Author should match");
Assert::IsTrue(metadata.copyright.has_value(), L"Copyright should be present");
Assert::IsTrue(metadata.copyright.value().find(L"Carl Seibert") != std::wstring::npos, L"Copyright should contain Carl Seibert");
}
TEST_METHOD(ExtractEXIF_ExifTest2_WidthHeight)
{
// Test exif_test_2.jpg which only contains width and height
WICMetadataExtractor extractor;
EXIFMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\exif_test_2.jpg";
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
Assert::IsTrue(result, L"EXIF extraction should succeed");
// exif_test_2.jpg only has width and height
Assert::IsTrue(metadata.width.has_value(), L"Width should be present");
Assert::AreEqual(1080u, static_cast<unsigned int>(metadata.width.value()), L"Width should be 1080px");
Assert::IsTrue(metadata.height.has_value(), L"Height should be present");
Assert::AreEqual(810u, static_cast<unsigned int>(metadata.height.value()), L"Height should be 810px");
// Other fields should not be present
Assert::IsFalse(metadata.cameraMake.has_value(), L"Camera make should not be present in exif_test_2.jpg");
Assert::IsFalse(metadata.cameraModel.has_value(), L"Camera model should not be present in exif_test_2.jpg");
Assert::IsFalse(metadata.iso.has_value(), L"ISO should not be present in exif_test_2.jpg");
}
TEST_METHOD(ExtractEXIF_ClearCache)
{
// Test cache clearing works
WICMetadataExtractor extractor;
EXIFMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg";
bool result1 = extractor.ExtractEXIFMetadata(testFile, metadata);
Assert::IsTrue(result1);
extractor.ClearCache();
EXIFMetadata metadata2;
bool result2 = extractor.ExtractEXIFMetadata(testFile, metadata2);
Assert::IsTrue(result2);
// Both calls should succeed
Assert::AreEqual(metadata.cameraMake.value().c_str(), metadata2.cameraMake.value().c_str());
}
};
TEST_CLASS(ExtractXMPMetadataTests)
{
public:
TEST_METHOD(ExtractXMP_InvalidFile_ReturnsFalse)
{
// Test that XMP extraction fails for nonexistent file
WICMetadataExtractor extractor;
XMPMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg";
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
Assert::IsFalse(result, L"XMP extraction should fail for nonexistent file");
}
TEST_METHOD(ExtractXMP_XmpTest_AllFields)
{
// Test xmp_test.jpg which contains comprehensive XMP data
WICMetadataExtractor extractor;
XMPMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
Assert::IsTrue(result, L"XMP extraction should succeed");
// Verify all the fields that are in xmp_test.jpg
Assert::IsTrue(metadata.title.has_value(), L"Title should be present");
Assert::AreEqual(L"object name here", metadata.title.value().c_str(), L"Title should match");
Assert::IsTrue(metadata.description.has_value(), L"Description should be present");
Assert::IsTrue(metadata.description.value().find(L"This is a metadata test file") != std::wstring::npos,
L"Description should contain expected text");
Assert::IsTrue(metadata.rights.has_value(), L"Rights should be present");
Assert::AreEqual(L"metadatamatters.blog", metadata.rights.value().c_str(), L"Rights should match");
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop Lightroom") != std::wstring::npos,
L"Creator tool should contain Lightroom");
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
L"Document ID should start with xmp.did:");
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
L"Instance ID should start with xmp.iid:");
Assert::IsTrue(metadata.subject.has_value(), L"Subject keywords should be present");
Assert::IsTrue(metadata.subject.value().size() > 0, L"Should have at least one keyword");
}
TEST_METHOD(ExtractXMP_XmpTest2_BasicFields)
{
// Test xmp_test_2.jpg which only contains basic XMP fields
WICMetadataExtractor extractor;
XMPMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\xmp_test_2.jpg";
bool result = extractor.ExtractXMPMetadata(testFile, metadata);
Assert::IsTrue(result, L"XMP extraction should succeed");
// xmp_test_2.jpg only has CreatorTool, DocumentID, and InstanceID
Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present");
Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop CS6") != std::wstring::npos,
L"Creator tool should be Photoshop CS6");
Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present");
Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos,
L"Document ID should start with xmp.did:");
Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present");
Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos,
L"Instance ID should start with xmp.iid:");
// Other fields should not be present
Assert::IsFalse(metadata.title.has_value(), L"Title should not be present in xmp_test_2.jpg");
Assert::IsFalse(metadata.description.has_value(), L"Description should not be present in xmp_test_2.jpg");
Assert::IsFalse(metadata.rights.has_value(), L"Rights should not be present in xmp_test_2.jpg");
Assert::IsFalse(metadata.creator.has_value(), L"Creator should not be present in xmp_test_2.jpg");
}
TEST_METHOD(ExtractXMP_ClearCache)
{
// Test cache clearing works
WICMetadataExtractor extractor;
XMPMetadata metadata;
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
Assert::IsTrue(result1);
extractor.ClearCache();
XMPMetadata metadata2;
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
Assert::IsTrue(result2);
// Both calls should succeed
Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str());
}
};
}

View File

@@ -0,0 +1,45 @@
# Test Data Attribution
This directory contains test image files used for PowerRename metadata extraction unit tests. These images are sourced from Wikimedia Commons and are used under the Creative Commons licenses specified below.
## Test Files and Licenses
### Files from Carlseibert
**License:** [Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/)
- `exif_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
- `xmp_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons
### Files from Edward Steven
**License:** [Creative Commons Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/)
- `exif_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
- `xmp_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons
## Acknowledgments
We gratefully acknowledge the contributions of Carlseibert and Edward Steven for making these images available under Creative Commons licenses. Their work enables us to test metadata extraction functionality with real-world EXIF and XMP data.
## Usage
These test images are used in PowerRename's unit tests to verify correct extraction of:
- EXIF metadata (camera make, model, ISO, aperture, shutter speed, etc.)
- XMP metadata (creator, title, description, copyright, etc.)
- GPS coordinates
- Date/time information
## License Compliance
These test images are distributed as part of the PowerToys source code repository under their original Creative Commons licenses:
- Files from Carlseibert: CC BY-SA 4.0
- Files from Edward Steven: CC BY-SA 2.0
**Modifications:** These images have not been modified from their original versions downloaded from Wikimedia Commons. They are used in their original form for metadata extraction testing purposes.
**Distribution:** These test images are included in the PowerToys source repository and comply with the terms of their respective Creative Commons licenses through proper attribution in this file. While included in the source code, these images are not distributed in end-user installation packages or releases.
**Derivatives:** Any modifications or derivative works of these images must comply with the respective CC BY-SA license terms, including proper attribution and applying the same license to the modified versions.
For more information about Creative Commons licenses, visit: https://creativecommons.org/licenses/

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

View File

@@ -29,7 +29,10 @@
Name="TextExtractorEnableToggleControlHeaderText"
x:Uid="TextExtractor_EnableToggleControl_HeaderText"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/TextExtractor.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.AutomationId="EnableTextExtractorToggleSwitch"
IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<InfoBar
@@ -48,12 +51,16 @@
Name="ActivationShortcut"
x:Uid="Activation_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="TextExtractorActivationShortcut"
HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="TextExtractorLanguages" x:Uid="TextExtractor_Languages">
<ComboBox
x:Name="TextExtractor_ComboBox"
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="TextExtractorLanguageComboBox"
DropDownOpened="TextExtractor_ComboBox_DropDownOpened"
ItemsSource="{x:Bind Path=ViewModel.AvailableLanguages, Mode=OneWay}"
Loaded="TextExtractor_ComboBox_Loaded"