diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt
index 756c450534..41a53d33ed 100644
--- a/.github/actions/spell-check/allow/code.txt
+++ b/.github/actions/spell-check/allow/code.txt
@@ -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
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 31dabc785a..63469ee8ee 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -171,9 +171,12 @@ BYPOSITION
CALCRECT
CALG
callbackptr
+cabstr
calpwstr
+caub
Cangjie
CANRENAME
+Carlseibert
Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
@@ -279,6 +282,7 @@ cpptools
cppvsdbg
cppwinrt
createdump
+creativecommons
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
@@ -341,6 +345,7 @@ Deact
debugbreak
decryptor
Dedup
+dfx
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
@@ -514,6 +519,7 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
+FNumber
FARPROC
fdx
fesf
@@ -697,6 +703,7 @@ HTCLIENT
hthumbnail
HTOUCHINPUT
HTTRANSPARENT
+hutchinsoniana
HVal
HValue
Hvci
@@ -718,7 +725,9 @@ IDCANCEL
IDD
idk
idl
+IIM
idlist
+ifd
IDOK
IDOn
IDR
@@ -735,6 +744,7 @@ Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
+IPTC
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
@@ -874,6 +884,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
+lon
LOGMSG
LOGPIXELSX
LOGPIXELSY
@@ -965,6 +976,7 @@ MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metadatas
+metadatamatters
metafile
mfc
Mgmt
@@ -1124,6 +1136,7 @@ NONCLIENTMETRICSW
NONELEVATED
nonspace
nonstd
+nullrefs
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
@@ -1280,6 +1293,7 @@ pnid
PNMLINK
Poc
Podcasts
+Photoshop
POINTERID
POINTERUPDATE
Pokedex
@@ -1482,6 +1496,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
+samsung
sancov
SAVEFAILED
scanled
@@ -1844,6 +1859,7 @@ USEINSTALLERFORTEST
USESHOWWINDOW
USESTDHANDLES
USRDLL
+utm
UType
uuidv
uwp
@@ -1933,6 +1949,7 @@ wgpocpl
WHEREID
wic
wifi
+wikimedia
wikipedia
WIL
winapi
@@ -2027,7 +2044,9 @@ XAxis
XButton
xclip
xcopy
+xap
XDeployment
+XDimension
xdf
XDocument
XElement
@@ -2045,6 +2064,7 @@ xsi
XSpeed
XStr
xstyler
+xmp
XTimer
XUP
XVIRTUALSCREEN
@@ -2052,6 +2072,7 @@ xxxxxx
YAxis
ycombinator
YIncrement
+YDimension
yinle
yinyue
YPels
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
index 16c6b4efbc..74d87c5623 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
@@ -92,6 +92,7 @@
+
@@ -99,7 +100,7 @@
-
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
index 5018653070..711063d0cb 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp
new file mode 100644
index 0000000000..945a516ca4
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp
@@ -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
+
+// 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;
+}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h
new file mode 100644
index 0000000000..540478856e
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h
@@ -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);
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
index 6685afafc2..c650439b65 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
@@ -110,6 +110,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
+
@@ -127,7 +128,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
-
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
index 63f23c3e86..1d511f2afe 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
@@ -1,7 +1,7 @@
#pragma once
#include
-#include "..\..\powerrename\lib\Helpers.h"
+#include "Helpers.h"
#include "helpers_filesystem.h"
#pragma comment(lib, "Pathcch.lib")
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
index 50f92562d2..02874ab8f3 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
@@ -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);
}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
index b766a837d5..13093e1d08 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM
@@ -13,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
index a7ddfe835f..a178e00195 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
@@ -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;
}
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
index 1a7ca91972..3afb41f546 100644
--- a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
@@ -52,7 +52,7 @@
true
true
true
- legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+ legacy_stdio_definitions.lib;windowscodecs.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
diff --git a/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj b/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj
index af3c71ad8e..a101c28ac9 100644
--- a/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj
+++ b/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj
@@ -51,7 +51,7 @@
Windows
true
false
- runtimeobject.lib;%(AdditionalDependencies)
+ runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)
Source.def
@@ -75,7 +75,7 @@ MakeAppx.exe pack /d . /p $(OutDir)PowerRenameContextMenuPackage.msix /nvtrue
true
false
- runtimeobject.lib;%(AdditionalDependencies)
+ runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies)
Source.def
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
index df105e5815..eb21a94049 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
@@ -79,7 +79,7 @@
_DEBUG;%(PreprocessorDefinitions)
- kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)
+ kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)
@@ -89,7 +89,7 @@
true
true
- kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies)
+ kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp
index 67f1834499..b46c5e548d 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp
@@ -1,4 +1,4 @@
-#include "pch.h"
+#include "pch.h"
#include "App.xaml.h"
#include "MainWindow.xaml.h"
@@ -117,6 +117,9 @@ App::App()
/// Details about the launch request and process.
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();
window.Activate();
-}
\ No newline at end of file
+}
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl
index 041e3d1921..bb02ec2e14 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl
@@ -16,6 +16,7 @@ namespace PowerRenameUI
Windows.Foundation.Collections.IObservableVector DateTimeShortcuts { get; };
Windows.Foundation.Collections.IObservableVector CounterShortcuts { get; };
Windows.Foundation.Collections.IObservableVector RandomizerShortcuts { get; };
+ Windows.Foundation.Collections.IObservableVector MetadataShortcuts { get; };
String OriginalCount;
String RenamedCount;
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml
index 7126a63604..1c67d9a73b 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml
@@ -330,6 +330,8 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -560,31 +604,61 @@
FontFamily="{ThemeResource SymbolThemeFontFamily}" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp
index 7cc3bf9543..01c7c517c2 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp
@@ -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
#include
+#include
#include
#include
@@ -225,6 +226,11 @@ namespace winrt::PowerRenameUI::implementation
m_RandomizerShortcuts.Append(winrt::make(L"${rstringdigit=36}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Digit").ValueAsString()));
m_RandomizerShortcuts.Append(winrt::make(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();
+ // 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(m_explorerItems)->SetIsFiltered(false);
- InvalidateItemListViewState();
+ DWORD filter = 0;
+ m_prManager->GetFilter(&filter);
+ if (filter != PowerRenameFilters::None)
+ {
+ m_prManager->SwitchFilter(0);
+ get_self(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(m_explorerItems)->SetIsFiltered(true);
- InvalidateItemListViewState();
+ DWORD filter = 0;
+ m_prManager->GetFilter(&filter);
+ if (filter != PowerRenameFilters::ShouldRename)
+ {
+ m_prManager->SwitchFilter(0);
+ UpdateCounts();
+ get_self(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();
+ 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(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 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(patternWithDollar, description));
+ }
+ catch (...) {
+ // If resource doesn't exist, use the pattern name as description
+ m_metadataShortcuts.Append(winrt::make(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(std::toupper(ch));
+ capitalizeNext = false;
+ }
+ else
+ {
+ result += static_cast(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;
}
}
+
+
+
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h
index 8c70194f1b..cff802f582 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h
@@ -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
#include
#include
+#include
+#include
namespace winrt::PowerRenameUI::implementation
{
@@ -88,6 +90,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector DateTimeShortcuts() { return m_dateTimeShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector CounterShortcuts() { return m_CounterShortcuts; }
winrt::Windows::Foundation::Collections::IObservableVector RandomizerShortcuts() { return m_RandomizerShortcuts; }
+ winrt::Windows::Foundation::Collections::IObservableVector MetadataShortcuts() { return m_metadataShortcuts; }
hstring OriginalCount();
void OriginalCount(hstring value);
@@ -111,6 +114,7 @@ namespace winrt::PowerRenameUI::implementation
winrt::Windows::Foundation::Collections::IObservableVector m_dateTimeShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector m_CounterShortcuts;
winrt::Windows::Foundation::Collections::IObservableVector m_RandomizerShortcuts;
+ winrt::Windows::Foundation::Collections::IObservableVector 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
{
};
}
+
diff --git a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw
index 9af9e2365b..178106908d 100644
--- a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw
+++ b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw
@@ -414,6 +414,9 @@
Time used for replacement
+
+ Metadata source for replacement
+
Creation Time
@@ -423,4 +426,149 @@
Access Time
+
+
+ EXIF Metadata
+
+
+ XMP Metadata
+
+
+
+ Replace with media metadata
+
+
+ Camera manufacturer name
+
+
+ Camera model name
+
+
+ Lens model name
+
+
+ ISO sensitivity value
+
+
+ F-number aperture value
+
+
+ Shutter speed value
+
+
+ Focal length in millimeters
+
+
+ Flash status (On/Off)
+
+
+ Image width in pixels
+
+
+ Image height in pixels
+
+
+ Image author/artist
+
+
+ Copyright information
+
+
+ GPS latitude coordinate
+
+
+ GPS longitude coordinate
+
+
+ GPS altitude in meters
+
+
+ Exposure compensation value
+
+
+ Image orientation
+
+
+ Color space information
+
+
+ Year photo was taken (4 digits)
+
+
+ Year photo was taken (2 digits)
+
+
+ Month photo was taken (01-12)
+
+
+ Day photo was taken (01-31)
+
+
+ Hour photo was taken (00-23)
+
+
+ Minute photo was taken (00-59)
+
+
+ Second photo was taken (00-59)
+
+
+ Year from XMP create date (4 digits)
+
+
+ Year from XMP create date (2 digits)
+
+
+ Month from XMP create date (01-12)
+
+
+ Day from XMP create date (01-31)
+
+
+ Hour from XMP create date (00-23)
+
+
+ Minute from XMP create date (00-59)
+
+
+ Second from XMP create date (00-59)
+
+
+
+
+ Software used to create/edit
+
+
+
+
+ Document title
+
+
+ Document description
+
+
+ Document creator/author
+
+
+ Keywords/tags
+
+
+
+
+ Copyright/rights information
+
+
+
+
+ Document unique identifier
+
+
+ Instance unique identifier
+
+
+ Original document identifier
+
+
+ Version identifier
+
\ No newline at end of file
diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
index 9c612a08ff..2364012861 100644
--- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj
+++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
@@ -24,7 +24,7 @@
..\lib\;..\PowerRenameUILib\;..\;..\..\..\;..\..\..\common\telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
- Pathcch.lib;comctl32.lib;shcore.lib;%(AdditionalDependencies)
+ Pathcch.lib;comctl32.lib;shcore.lib;windowscodecs.lib;%(AdditionalDependencies)
PowerRenameExt.def
gdi32.dll;shell32.dll;ole32.dll;shlwapi.dll;oleaut32.dll;%(DelayLoadDLLs)
diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp
index 2515a8a7ae..c3902c7b93 100644
--- a/src/modules/powerrename/lib/Helpers.cpp
+++ b/src/modules/powerrename/lib/Helpers.cpp
@@ -1,9 +1,13 @@
#include "pch.h"
#include "Helpers.h"
+#include "MetadataTypes.h"
#include
#include
#include
#include
+#include
+#include
+#include
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& 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 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 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"";
-}
\ No newline at end of file
+}
diff --git a/src/modules/powerrename/lib/Helpers.h b/src/modules/powerrename/lib/Helpers.h
index 05b8eab19d..83659637c9 100644
--- a/src/modules/powerrename/lib/Helpers.h
+++ b/src/modules/powerrename/lib/Helpers.h
@@ -1,13 +1,17 @@
#pragma once
#include "PowerRenameInterfaces.h"
+#include "MetadataTypes.h"
+#include "MetadataPatternExtractor.h"
#include
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);
diff --git a/src/modules/powerrename/lib/MetadataFormatHelper.cpp b/src/modules/powerrename/lib/MetadataFormatHelper.cpp
new file mode 100644
index 0000000000..e6b88e913a
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataFormatHelper.cpp
@@ -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
+#include
+#include
+
+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(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(absolute);
+ double minutes = (absolute - static_cast(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(rationalBytes[0]) |
+ (static_cast(rationalBytes[1]) << 8) |
+ (static_cast(rationalBytes[2]) << 16) |
+ (static_cast(rationalBytes[3]) << 24);
+
+ uint32_t denominator = static_cast(rationalBytes[4]) |
+ (static_cast(rationalBytes[5]) << 8) |
+ (static_cast(rationalBytes[6]) << 16) |
+ (static_cast(rationalBytes[7]) << 24);
+
+ if (denominator != 0)
+ {
+ return static_cast(numerator) / static_cast(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(rationalBytes[0]) |
+ (static_cast(rationalBytes[1]) << 8) |
+ (static_cast(rationalBytes[2]) << 16) |
+ (static_cast(rationalBytes[3]) << 24);
+
+ uint32_t denominator_uint = static_cast(rationalBytes[4]) |
+ (static_cast(rationalBytes[5]) << 8) |
+ (static_cast(rationalBytes[6]) << 16) |
+ (static_cast(rationalBytes[7]) << 24);
+
+ // Reinterpret as signed
+ int32_t numerator = static_cast(numerator_uint);
+ int32_t denominator = static_cast(denominator_uint);
+
+ if (denominator != 0)
+ {
+ return static_cast(numerator) / static_cast(denominator);
+ }
+
+ return 0.0;
+}
+
+std::pair 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;
+}
diff --git a/src/modules/powerrename/lib/MetadataFormatHelper.h b/src/modules/powerrename/lib/MetadataFormatHelper.h
new file mode 100644
index 0000000000..86208225cf
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataFormatHelper.h
@@ -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
+#include
+#include
+#include
+
+namespace PowerRenameLib
+{
+ ///
+ /// Helper class for formatting and parsing metadata values
+ /// Provides static utility functions for converting metadata to human-readable strings
+ /// and parsing raw metadata values
+ ///
+ class MetadataFormatHelper
+ {
+ public:
+ // Formatting functions - Convert metadata values to display strings
+
+ ///
+ /// Format aperture value (f-number)
+ ///
+ /// Aperture value (e.g., 2.8)
+ /// Formatted string (e.g., "f/2.8")
+ static std::wstring FormatAperture(double aperture);
+
+ ///
+ /// Format shutter speed
+ ///
+ /// Shutter speed in seconds
+ /// Formatted string (e.g., "1/100s" or "2.5s")
+ static std::wstring FormatShutterSpeed(double speed);
+
+ ///
+ /// Format ISO value
+ ///
+ /// ISO speed value
+ /// Formatted string (e.g., "ISO 400")
+ static std::wstring FormatISO(int64_t iso);
+
+ ///
+ /// Format flash status
+ ///
+ /// Flash value from EXIF
+ /// Formatted string (e.g., "Flash On" or "Flash Off")
+ static std::wstring FormatFlash(int64_t flashValue);
+
+ ///
+ /// Format GPS coordinate
+ ///
+ /// Coordinate value in decimal degrees
+ /// true for latitude, false for longitude
+ /// Formatted string (e.g., "40°26.76'N")
+ static std::wstring FormatCoordinate(double coord, bool isLatitude);
+
+ ///
+ /// Format SYSTEMTIME to string
+ ///
+ /// SYSTEMTIME structure
+ /// Formatted string (e.g., "2024-03-15 14:30:45")
+ static std::wstring FormatSystemTime(const SYSTEMTIME& st);
+
+ // Parsing functions - Convert raw metadata to usable values
+
+ ///
+ /// Parse GPS rational value from PROPVARIANT
+ ///
+ /// PROPVARIANT containing GPS rational data
+ /// Parsed double value
+ static double ParseGPSRational(const PROPVARIANT& pv);
+
+ ///
+ /// Parse single rational value from byte array
+ ///
+ /// Byte array containing rational data
+ /// Offset in the byte array
+ /// Parsed double value (numerator / denominator)
+ static double ParseSingleRational(const uint8_t* bytes, size_t offset);
+
+ ///
+ /// Parse single signed rational value from byte array
+ ///
+ /// Byte array containing signed rational data
+ /// Offset in the byte array
+ /// Parsed double value (signed numerator / signed denominator)
+ static double ParseSingleSRational(const uint8_t* bytes, size_t offset);
+
+ ///
+ /// Parse GPS coordinates from PROPVARIANT values
+ ///
+ /// PROPVARIANT containing latitude
+ /// PROPVARIANT containing longitude
+ /// PROPVARIANT containing latitude reference (N/S)
+ /// PROPVARIANT containing longitude reference (E/W)
+ /// Pair of (latitude, longitude) in decimal degrees
+ static std::pair ParseGPSCoordinates(
+ const PROPVARIANT& latitude,
+ const PROPVARIANT& longitude,
+ const PROPVARIANT& latRef,
+ const PROPVARIANT& lonRef);
+
+ ///
+ /// 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.
+ ///
+ /// String to sanitize
+ /// Sanitized string safe for use in filename
+ static std::wstring SanitizeForFileName(const std::wstring& str);
+ };
+}
diff --git a/src/modules/powerrename/lib/MetadataPatternExtractor.cpp b/src/modules/powerrename/lib/MetadataPatternExtractor.cpp
new file mode 100644
index 0000000000..cfbc40837d
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataPatternExtractor.cpp
@@ -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
+#include
+#include
+#include
+#include
+#include
+
+using namespace PowerRenameLib;
+
+MetadataPatternExtractor::MetadataPatternExtractor()
+ : extractor(std::make_unique())
+{
+}
+
+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(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 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 MetadataPatternExtractor::GetAllPossiblePatterns()
+{
+ auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF);
+ auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP);
+
+ std::vector 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;
+}
+
diff --git a/src/modules/powerrename/lib/MetadataPatternExtractor.h b/src/modules/powerrename/lib/MetadataPatternExtractor.h
new file mode 100644
index 0000000000..787e5c437d
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataPatternExtractor.h
@@ -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
+#include
+#include
+#include
+#include "MetadataTypes.h"
+
+namespace PowerRenameLib
+{
+ // Pattern-Value mapping for metadata replacement
+ using MetadataPatternMap = std::unordered_map;
+
+ ///
+ /// Metadata pattern extractor that converts metadata into replaceable patterns
+ ///
+ class MetadataPatternExtractor
+ {
+ public:
+ MetadataPatternExtractor();
+ ~MetadataPatternExtractor();
+
+ MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type);
+
+ void ClearCache();
+
+ static std::vector GetSupportedPatterns(MetadataType type);
+ static std::vector GetAllPossiblePatterns();
+
+ private:
+ std::unique_ptr extractor;
+
+ MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
+ MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
+ };
+}
diff --git a/src/modules/powerrename/lib/MetadataResultCache.cpp b/src/modules/powerrename/lib/MetadataResultCache.cpp
new file mode 100644
index 0000000000..5f30b24abe
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataResultCache.cpp
@@ -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
+ 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>(filePath, outMetadata, exifCache, exifMutex, loader);
+}
+
+bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath,
+ XMPMetadata& outMetadata,
+ const XMPLoader& loader)
+{
+ return GetOrLoadInternal>(filePath, outMetadata, xmpCache, xmpMutex, loader);
+}
+
+void MetadataResultCache::ClearAll()
+{
+ {
+ std::unique_lock lock(exifMutex);
+ exifCache.clear();
+ }
+
+ {
+ std::unique_lock lock(xmpMutex);
+ xmpCache.clear();
+ }
+}
diff --git a/src/modules/powerrename/lib/MetadataResultCache.h b/src/modules/powerrename/lib/MetadataResultCache.h
new file mode 100644
index 0000000000..ad3b9782c4
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataResultCache.h
@@ -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
+#include
+#include
+#include
+
+namespace PowerRenameLib
+{
+ class MetadataResultCache
+ {
+ public:
+ using EXIFLoader = std::function;
+ using XMPLoader = std::function;
+
+ 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
+ struct CacheEntry
+ {
+ bool wasSuccessful;
+ T data;
+ };
+
+ mutable std::shared_mutex exifMutex;
+ mutable std::shared_mutex xmpMutex;
+ std::unordered_map> exifCache;
+ std::unordered_map> xmpCache;
+ };
+}
diff --git a/src/modules/powerrename/lib/MetadataTypes.h b/src/modules/powerrename/lib/MetadataTypes.h
new file mode 100644
index 0000000000..aa6a721e4c
--- /dev/null
+++ b/src/modules/powerrename/lib/MetadataTypes.h
@@ -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
+#include
+#include
+#include
+
+namespace PowerRenameLib
+{
+ ///
+ /// Supported metadata format types
+ ///
+ enum class MetadataType
+ {
+ EXIF, // EXIF metadata (camera settings, date taken, etc.)
+ XMP // XMP metadata (Dublin Core, Photoshop, etc.)
+ };
+
+ ///
+ /// Complete EXIF metadata structure
+ /// Contains all commonly used EXIF fields with optional values
+ ///
+ struct EXIFMetadata
+ {
+ // Date and time information
+ std::optional dateTaken; // DateTimeOriginal
+ std::optional dateDigitized; // DateTimeDigitized
+ std::optional dateModified; // DateTime
+
+ // Camera information
+ std::optional cameraMake; // Make
+ std::optional cameraModel; // Model
+ std::optional lensModel; // LensModel
+
+ // Shooting parameters
+ std::optional iso; // ISO speed
+ std::optional aperture; // F-number
+ std::optional shutterSpeed; // Exposure time
+ std::optional focalLength; // Focal length in mm
+ std::optional exposureBias; // Exposure bias value
+ std::optional flash; // Flash status
+
+ // Image properties
+ std::optional width; // Image width in pixels
+ std::optional height; // Image height in pixels
+ std::optional orientation; // Image orientation
+ std::optional colorSpace; // Color space
+
+ // Author and copyright
+ std::optional author; // Artist
+ std::optional copyright; // Copyright notice
+
+ // GPS information
+ std::optional latitude; // GPS latitude in decimal degrees
+ std::optional longitude; // GPS longitude in decimal degrees
+ std::optional altitude; // GPS altitude in meters
+ };
+
+ ///
+ /// XMP (Extensible Metadata Platform) metadata structure
+ /// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields
+ ///
+ struct XMPMetadata
+ {
+ // XMP Basic schema - https://ns.adobe.com/xap/1.0/
+ std::optional createDate; // xmp:CreateDate
+ std::optional modifyDate; // xmp:ModifyDate
+ std::optional metadataDate; // xmp:MetadataDate
+ std::optional creatorTool; // xmp:CreatorTool
+
+ // Dublin Core schema - http://purl.org/dc/elements/1.1/
+ std::optional title; // dc:title
+ std::optional description; // dc:description
+ std::optional creator; // dc:creator (author)
+ std::optional> subject; // dc:subject (keywords)
+
+ // XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/
+ std::optional rights; // xmpRights:WebStatement (copyright)
+
+ // XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/
+ std::optional documentID; // xmpMM:DocumentID
+ std::optional instanceID; // xmpMM:InstanceID
+ std::optional originalDocumentID; // xmpMM:OriginalDocumentID
+ std::optional versionID; // xmpMM:VersionID
+ };
+
+
+
+
+ ///
+ /// Constants for metadata pattern names
+ ///
+ 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";
+ }
+}
\ No newline at end of file
diff --git a/src/modules/powerrename/lib/PowerRenameInterfaces.h b/src/modules/powerrename/lib/PowerRenameInterfaces.h
index ea761d156a..7e3402433b 100644
--- a/src/modules/powerrename/lib/PowerRenameInterfaces.h
+++ b/src/modules/powerrename/lib/PowerRenameInterfaces.h
@@ -1,7 +1,10 @@
#pragma once
#include "pch.h"
+#include "MetadataTypes.h"
+#include "MetadataPatternExtractor.h"
#include
#include
+#include
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;
};
diff --git a/src/modules/powerrename/lib/PowerRenameItem.cpp b/src/modules/powerrename/lib/PowerRenameItem.cpp
index b33fccfb89..61e07a93fc 100644
--- a/src/modules/powerrename/lib/PowerRenameItem.cpp
+++ b/src/modules/powerrename/lib/PowerRenameItem.cpp
@@ -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;
diff --git a/src/modules/powerrename/lib/PowerRenameLib.vcxproj b/src/modules/powerrename/lib/PowerRenameLib.vcxproj
index 103eab8e8e..bd5740dee7 100644
--- a/src/modules/powerrename/lib/PowerRenameLib.vcxproj
+++ b/src/modules/powerrename/lib/PowerRenameLib.vcxproj
@@ -16,19 +16,24 @@
-
-
-
..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\
+ $(ProjectDir)..\..\..\..\deps
+
+
+
Level3
WIN32;_LIB;%(PreprocessorDefinitions)
$(ProjectDir)..\;$(ProjectDir)..\ui;$(ProjectDir)..\dll;$(ProjectDir)..\lib;$(ProjectDir)..\..\..\;$(ProjectDir)..\..\..\common\Telemetry;%(AdditionalIncludeDirectories);$(GeneratedFilesDir)
+ /FS %(AdditionalOptions)
+
+ windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)
+
@@ -47,6 +52,12 @@
+
+
+
+
+
+
@@ -64,6 +75,10 @@
Create
+
+
+
+
diff --git a/src/modules/powerrename/lib/PowerRenameManager.cpp b/src/modules/powerrename/lib/PowerRenameManager.cpp
index b6641374ba..160d064e09 100644
--- a/src/modules/powerrename/lib/PowerRenameManager.cpp
+++ b/src/modules/powerrename/lib/PowerRenameManager.cpp
@@ -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;
diff --git a/src/modules/powerrename/lib/PowerRenameManager.h b/src/modules/powerrename/lib/PowerRenameManager.h
index f339f5c9d4..a9fa44d144 100644
--- a/src/modules/powerrename/lib/PowerRenameManager.h
+++ b/src/modules/powerrename/lib/PowerRenameManager.h
@@ -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);
diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.cpp b/src/modules/powerrename/lib/PowerRenameRegEx.cpp
index 34d3cc5c0c..567df48606 100644
--- a/src/modules/powerrename/lib/PowerRenameRegEx.cpp
+++ b/src/modules/powerrename/lib/PowerRenameRegEx.cpp
@@ -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();
+}
+
+
diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.h b/src/modules/powerrename/lib/PowerRenameRegEx.h
index 55c6c14c17..9e43107efa 100644
--- a/src/modules/powerrename/lib/PowerRenameRegEx.h
+++ b/src/modules/powerrename/lib/PowerRenameRegEx.h
@@ -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;
diff --git a/src/modules/powerrename/lib/PropVariantValue.h b/src/modules/powerrename/lib/PropVariantValue.h
new file mode 100644
index 0000000000..23e5973d25
--- /dev/null
+++ b/src/modules/powerrename/lib/PropVariantValue.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include
+#include
+
+namespace PowerRenameLib
+{
+ ///
+ /// RAII wrapper around PROPVARIANT to ensure proper initialization and cleanup.
+ /// Move-only semantics keep ownership simple while still allowing use in optionals.
+ ///
+ 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;
+ };
+}
diff --git a/src/modules/powerrename/lib/Renaming.cpp b/src/modules/powerrename/lib/Renaming.cpp
index aa21666783..028621eef4 100644
--- a/src/modules/powerrename/lib/Renaming.cpp
+++ b/src/modules/powerrename/lib/Renaming.cpp
@@ -1,9 +1,13 @@
#include "pch.h"
#include
+#include
+#include
+#include
#include "Renaming.h"
#include
-
+#include "MetadataPatternExtractor.h"
+#include "PowerRenameRegEx.h"
namespace fs = std::filesystem;
bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnumIndex, CComPtr& spItem)
@@ -14,6 +18,7 @@ bool DoRename(CComPtr& 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& spRenameRegEx, unsigned long& itemEnum
{
useFileTime = true;
}
- CoTaskMemFree(replaceTerm);
int id = -1;
winrt::check_hresult(spItem->GetId(&id));
@@ -30,6 +34,29 @@ bool DoRename(CComPtr& 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& 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 s_metadataExtractor;
+ static std::optional s_activeMetadataType;
+
+ // Initialize the extractor only once
+ std::call_once(s_metadataExtractorInitFlag, []() {
+ s_metadataExtractor = std::make_shared();
+ });
+
+ // Protect access to shared state
+ {
+ std::lock_guard 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& 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& spRenameRegEx, unsigned long& itemEnum
CoTaskMemFree(originalName);
return wouldRename;
-}
\ No newline at end of file
+}
diff --git a/src/modules/powerrename/lib/WICMetadataExtractor.cpp b/src/modules/powerrename/lib/WICMetadataExtractor.cpp
new file mode 100644
index 0000000000..bd2f9c08dc
--- /dev/null
+++ b/src/modules/powerrename/lib/WICMetadataExtractor.cpp
@@ -0,0 +1,1021 @@
+// 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 "WICMetadataExtractor.h"
+#include "MetadataFormatHelper.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace PowerRenameLib;
+
+namespace
+{
+ // Documentation: https://learn.microsoft.com/en-us/windows/win32/wic/-wic-native-image-format-metadata-queries
+
+ // WIC metadata property paths
+ const std::wstring EXIF_DATE_TAKEN = L"/app1/ifd/exif/{ushort=36867}"; // DateTimeOriginal
+ const std::wstring EXIF_DATE_DIGITIZED = L"/app1/ifd/exif/{ushort=36868}"; // DateTimeDigitized
+ const std::wstring EXIF_DATE_MODIFIED = L"/app1/ifd/{ushort=306}"; // DateTime
+ const std::wstring EXIF_CAMERA_MAKE = L"/app1/ifd/{ushort=271}"; // Make
+ const std::wstring EXIF_CAMERA_MODEL = L"/app1/ifd/{ushort=272}"; // Model
+ const std::wstring EXIF_LENS_MODEL = L"/app1/ifd/exif/{ushort=42036}"; // LensModel
+ const std::wstring EXIF_ISO = L"/app1/ifd/exif/{ushort=34855}"; // ISOSpeedRatings
+ const std::wstring EXIF_APERTURE = L"/app1/ifd/exif/{ushort=33437}"; // FNumber
+ const std::wstring EXIF_SHUTTER_SPEED = L"/app1/ifd/exif/{ushort=33434}"; // ExposureTime
+ const std::wstring EXIF_FOCAL_LENGTH = L"/app1/ifd/exif/{ushort=37386}"; // FocalLength
+ const std::wstring EXIF_EXPOSURE_BIAS = L"/app1/ifd/exif/{ushort=37380}"; // ExposureBiasValue
+ const std::wstring EXIF_FLASH = L"/app1/ifd/exif/{ushort=37385}"; // Flash
+ const std::wstring EXIF_ORIENTATION = L"/app1/ifd/{ushort=274}"; // Orientation
+ const std::wstring EXIF_COLOR_SPACE = L"/app1/ifd/exif/{ushort=40961}"; // ColorSpace
+ const std::wstring EXIF_WIDTH = L"/app1/ifd/exif/{ushort=40962}"; // PixelXDimension - actual image width
+ const std::wstring EXIF_HEIGHT = L"/app1/ifd/exif/{ushort=40963}"; // PixelYDimension - actual image height
+ const std::wstring EXIF_ARTIST = L"/app1/ifd/{ushort=315}"; // Artist
+ const std::wstring EXIF_COPYRIGHT = L"/app1/ifd/{ushort=33432}"; // Copyright
+
+ // GPS paths
+ const std::wstring GPS_LATITUDE = L"/app1/ifd/gps/{ushort=2}"; // GPSLatitude
+ const std::wstring GPS_LATITUDE_REF = L"/app1/ifd/gps/{ushort=1}"; // GPSLatitudeRef
+ const std::wstring GPS_LONGITUDE = L"/app1/ifd/gps/{ushort=4}"; // GPSLongitude
+ const std::wstring GPS_LONGITUDE_REF = L"/app1/ifd/gps/{ushort=3}"; // GPSLongitudeRef
+ const std::wstring GPS_ALTITUDE = L"/app1/ifd/gps/{ushort=6}"; // GPSAltitude
+ const std::wstring GPS_ALTITUDE_REF = L"/app1/ifd/gps/{ushort=5}"; // GPSAltitudeRef
+
+
+ // Documentation: https://developer.adobe.com/xmp/docs/XMPNamespaces/xmp/
+ // Based on actual WIC path format discovered through enumeration
+ // XMP Basic schema - xmp: namespace
+ const std::wstring XMP_CREATE_DATE = L"/xmp/xmp:CreateDate"; // XMP Create Date
+ const std::wstring XMP_MODIFY_DATE = L"/xmp/xmp:ModifyDate"; // XMP Modify Date
+ const std::wstring XMP_METADATA_DATE = L"/xmp/xmp:MetadataDate"; // XMP Metadata Date
+ const std::wstring XMP_CREATOR_TOOL = L"/xmp/xmp:CreatorTool"; // XMP Creator Tool
+
+ // Dublin Core schema - dc: namespace
+ // Note: For language alternatives like title/description, we need to append /x-default
+ const std::wstring XMP_DC_TITLE = L"/xmp/dc:title/x-default"; // Title (default language)
+ const std::wstring XMP_DC_DESCRIPTION = L"/xmp/dc:description/x-default"; // Description (default language)
+ const std::wstring XMP_DC_CREATOR = L"/xmp/dc:creator"; // Creator/Author
+ const std::wstring XMP_DC_SUBJECT = L"/xmp/dc:subject"; // Subject/Keywords (array)
+
+ // XMP Rights Management schema - xmpRights: namespace
+ const std::wstring XMP_RIGHTS = L"/xmp/xmpRights:WebStatement"; // Copyright/Rights
+
+ // XMP Media Management schema - xmpMM: namespace
+ const std::wstring XMP_MM_DOCUMENT_ID = L"/xmp/xmpMM:DocumentID"; // Document ID
+ const std::wstring XMP_MM_INSTANCE_ID = L"/xmp/xmpMM:InstanceID"; // Instance ID
+ const std::wstring XMP_MM_ORIGINAL_DOCUMENT_ID = L"/xmp/xmpMM:OriginalDocumentID"; // Original Document ID
+ const std::wstring XMP_MM_VERSION_ID = L"/xmp/xmpMM:VersionID"; // Version ID
+
+
+ std::wstring TrimWhitespace(const std::wstring& value)
+ {
+ const auto first = value.find_first_not_of(L" \t\r\n");
+ if (first == std::wstring::npos)
+ {
+ return {};
+ }
+
+ const auto last = value.find_last_not_of(L" \t\r\n");
+ return value.substr(first, last - first + 1);
+ }
+
+ bool TryParseFixedWidthInt(const std::wstring& source, size_t start, size_t length, int& value)
+ {
+ if (start + length > source.size())
+ {
+ return false;
+ }
+
+ int result = 0;
+ for (size_t i = 0; i < length; ++i)
+ {
+ const wchar_t ch = source[start + i];
+ if (ch < L'0' || ch > L'9')
+ {
+ return false;
+ }
+
+ result = result * 10 + static_cast(ch - L'0');
+ }
+
+ value = result;
+ return true;
+ }
+
+ bool ValidateAndBuildSystemTime(int year, int month, int day, int hour, int minute, int second, int milliseconds, SYSTEMTIME& outTime)
+ {
+ if (year < 1601 || year > 9999 ||
+ month < 1 || month > 12 ||
+ day < 1 || day > 31 ||
+ hour < 0 || hour > 23 ||
+ minute < 0 || minute > 59 ||
+ second < 0 || second > 59 ||
+ milliseconds < 0 || milliseconds > 999)
+ {
+ return false;
+ }
+
+ SYSTEMTIME candidate{};
+ candidate.wYear = static_cast(year);
+ candidate.wMonth = static_cast(month);
+ candidate.wDay = static_cast(day);
+ candidate.wHour = static_cast(hour);
+ candidate.wMinute = static_cast(minute);
+ candidate.wSecond = static_cast(second);
+ candidate.wMilliseconds = static_cast(milliseconds);
+
+ FILETIME fileTime{};
+ if (!SystemTimeToFileTime(&candidate, &fileTime))
+ {
+ return false;
+ }
+
+ outTime = candidate;
+ return true;
+ }
+
+ std::optional ParseExifDateTime(const std::wstring& date)
+ {
+ if (date.size() < 19)
+ {
+ return std::nullopt;
+ }
+
+ if (date[4] != L':' || date[7] != L':' ||
+ (date[10] != L' ' && date[10] != L'T') ||
+ date[13] != L':' || date[16] != L':')
+ {
+ return std::nullopt;
+ }
+
+ int year = 0;
+ int month = 0;
+ int day = 0;
+ int hour = 0;
+ int minute = 0;
+ int second = 0;
+
+ if (!TryParseFixedWidthInt(date, 0, 4, year) ||
+ !TryParseFixedWidthInt(date, 5, 2, month) ||
+ !TryParseFixedWidthInt(date, 8, 2, day) ||
+ !TryParseFixedWidthInt(date, 11, 2, hour) ||
+ !TryParseFixedWidthInt(date, 14, 2, minute) ||
+ !TryParseFixedWidthInt(date, 17, 2, second))
+ {
+ return std::nullopt;
+ }
+
+ int milliseconds = 0;
+ size_t pos = 19;
+ if (pos < date.size() && (date[pos] == L'.' || date[pos] == L','))
+ {
+ ++pos;
+ int digits = 0;
+ while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3)
+ {
+ milliseconds = milliseconds * 10 + static_cast(date[pos] - L'0');
+ ++pos;
+ ++digits;
+ }
+
+ while (digits > 0 && digits < 3)
+ {
+ milliseconds *= 10;
+ ++digits;
+ }
+ }
+
+ SYSTEMTIME result{};
+ if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, result))
+ {
+ return std::nullopt;
+ }
+
+ return result;
+ }
+
+ std::optional ParseIso8601DateTime(const std::wstring& date)
+ {
+ if (date.size() < 19)
+ {
+ return std::nullopt;
+ }
+
+ size_t separator = date.find(L'T');
+ if (separator == std::wstring::npos)
+ {
+ separator = date.find(L' ');
+ }
+
+ if (separator == std::wstring::npos)
+ {
+ return std::nullopt;
+ }
+
+ int year = 0;
+ int month = 0;
+ int day = 0;
+ if (!TryParseFixedWidthInt(date, 0, 4, year) ||
+ date[4] != L'-' ||
+ !TryParseFixedWidthInt(date, 5, 2, month) ||
+ date[7] != L'-' ||
+ !TryParseFixedWidthInt(date, 8, 2, day))
+ {
+ return std::nullopt;
+ }
+
+ size_t timePos = separator + 1;
+ if (timePos + 7 >= date.size())
+ {
+ return std::nullopt;
+ }
+
+ int hour = 0;
+ int minute = 0;
+ int second = 0;
+ if (!TryParseFixedWidthInt(date, timePos, 2, hour) ||
+ date[timePos + 2] != L':' ||
+ !TryParseFixedWidthInt(date, timePos + 3, 2, minute) ||
+ date[timePos + 5] != L':' ||
+ !TryParseFixedWidthInt(date, timePos + 6, 2, second))
+ {
+ return std::nullopt;
+ }
+
+ size_t pos = timePos + 8;
+ int milliseconds = 0;
+ if (pos < date.size() && (date[pos] == L'.' || date[pos] == L','))
+ {
+ ++pos;
+ int digits = 0;
+ while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3)
+ {
+ milliseconds = milliseconds * 10 + static_cast(date[pos] - L'0');
+ ++pos;
+ ++digits;
+ }
+
+ while (pos < date.size() && std::iswdigit(date[pos]))
+ {
+ ++pos;
+ }
+
+ while (digits > 0 && digits < 3)
+ {
+ milliseconds *= 10;
+ ++digits;
+ }
+ }
+
+ bool hasOffset = false;
+ int offsetMinutes = 0;
+ if (pos < date.size())
+ {
+ const wchar_t tzIndicator = date[pos];
+ if (tzIndicator == L'Z' || tzIndicator == L'z')
+ {
+ hasOffset = true;
+ offsetMinutes = 0;
+ ++pos;
+ }
+ else if (tzIndicator == L'+' || tzIndicator == L'-')
+ {
+ hasOffset = true;
+ const int sign = (tzIndicator == L'-') ? -1 : 1;
+ ++pos;
+
+ int offsetHours = 0;
+ int offsetMins = 0;
+ if (!TryParseFixedWidthInt(date, pos, 2, offsetHours))
+ {
+ return std::nullopt;
+ }
+ pos += 2;
+
+ if (pos < date.size() && date[pos] == L':')
+ {
+ ++pos;
+ }
+
+ if (pos + 1 < date.size() && std::iswdigit(date[pos]) && std::iswdigit(date[pos + 1]))
+ {
+ if (!TryParseFixedWidthInt(date, pos, 2, offsetMins))
+ {
+ return std::nullopt;
+ }
+ pos += 2;
+ }
+
+ if (offsetHours < 0 || offsetHours > 23 || offsetMins < 0 || offsetMins > 59)
+ {
+ return std::nullopt;
+ }
+
+ offsetMinutes = sign * (offsetHours * 60 + offsetMins);
+ }
+
+ while (pos < date.size() && std::iswspace(date[pos]))
+ {
+ ++pos;
+ }
+
+ if (pos != date.size())
+ {
+ return std::nullopt;
+ }
+ }
+
+ SYSTEMTIME baseTime{};
+ if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, baseTime))
+ {
+ return std::nullopt;
+ }
+
+ if (!hasOffset)
+ {
+ return baseTime;
+ }
+
+ FILETIME utcFileTime{};
+ if (!SystemTimeToFileTime(&baseTime, &utcFileTime))
+ {
+ return std::nullopt;
+ }
+
+ ULARGE_INTEGER timeValue{};
+ timeValue.LowPart = utcFileTime.dwLowDateTime;
+ timeValue.HighPart = utcFileTime.dwHighDateTime;
+
+ constexpr long long TicksPerMinute = 60LL * 10000000LL;
+ timeValue.QuadPart -= static_cast(offsetMinutes) * TicksPerMinute;
+
+ FILETIME adjustedUtc{};
+ adjustedUtc.dwLowDateTime = timeValue.LowPart;
+ adjustedUtc.dwHighDateTime = timeValue.HighPart;
+
+ FILETIME localFileTime{};
+ if (!FileTimeToLocalFileTime(&adjustedUtc, &localFileTime))
+ {
+ return std::nullopt;
+ }
+
+ SYSTEMTIME localTime{};
+ if (!FileTimeToSystemTime(&localFileTime, &localTime))
+ {
+ return std::nullopt;
+ }
+
+ return localTime;
+ }
+// Global WIC factory management with thread-safe access
+ CComPtr g_wicFactory;
+ std::once_flag g_wicInitFlag;
+ std::mutex g_wicFactoryMutex; // Protect access to g_wicFactory
+}
+
+WICMetadataExtractor::WICMetadataExtractor()
+{
+ InitializeWIC();
+}
+
+WICMetadataExtractor::~WICMetadataExtractor()
+{
+ // WIC cleanup handled statically
+}
+
+void WICMetadataExtractor::InitializeWIC()
+{
+ std::call_once(g_wicInitFlag, []() {
+ // Don't initialize COM in library code - assume caller has done it
+ // Just create the WIC factory
+ HRESULT hr = CoCreateInstance(
+ CLSID_WICImagingFactory,
+ nullptr,
+ CLSCTX_INPROC_SERVER,
+ IID_IWICImagingFactory,
+ reinterpret_cast(&g_wicFactory)
+ );
+
+ if (FAILED(hr))
+ {
+ g_wicFactory = nullptr;
+ }
+ });
+}
+
+CComPtr WICMetadataExtractor::GetWICFactory()
+{
+ std::lock_guard lock(g_wicFactoryMutex);
+ return g_wicFactory;
+}
+
+bool WICMetadataExtractor::ExtractEXIFMetadata(
+ const std::wstring& filePath,
+ EXIFMetadata& outMetadata)
+{
+ return cache.GetOrLoadEXIF(filePath, outMetadata, [this, &filePath](EXIFMetadata& metadata) {
+ return LoadEXIFMetadata(filePath, metadata);
+ });
+}
+
+bool WICMetadataExtractor::LoadEXIFMetadata(
+ const std::wstring& filePath,
+ EXIFMetadata& outMetadata)
+{
+ CComPtr reader;
+
+ if (!PathFileExistsW(filePath.c_str()))
+ {
+#ifdef _DEBUG
+ std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: File not found - " + filePath + L"\n";
+ OutputDebugStringW(msg.c_str());
+#endif
+ return false;
+ }
+
+ auto decoder = CreateDecoder(filePath);
+ if (!decoder)
+ {
+#ifdef _DEBUG
+ std::wstring msg = L"[PowerRename] EXIF metadata extraction: Unsupported format or unable to create decoder - " + filePath + L"\n";
+ OutputDebugStringW(msg.c_str());
+#endif
+ return false;
+ }
+
+ CComPtr frame;
+ if (FAILED(decoder->GetFrame(0, &frame)))
+ {
+#ifdef _DEBUG
+ std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: WIC decoder error - " + filePath + L"\n";
+ OutputDebugStringW(msg.c_str());
+#endif
+ return false;
+ }
+
+ reader = GetMetadataReader(decoder);
+ if (!reader)
+ {
+ // No metadata is not necessarily an error - just means the file has no EXIF data
+ return false;
+ }
+
+ ExtractAllEXIFFields(reader, outMetadata);
+ ExtractGPSData(reader, outMetadata);
+
+ return true;
+}
+
+void WICMetadataExtractor::ClearCache()
+{
+ cache.ClearAll();
+}
+
+CComPtr WICMetadataExtractor::CreateDecoder(const std::wstring& filePath)
+{
+ auto factory = GetWICFactory();
+ if (!factory)
+ {
+ return nullptr;
+ }
+
+ CComPtr decoder;
+ HRESULT hr = factory->CreateDecoderFromFilename(
+ filePath.c_str(),
+ nullptr,
+ GENERIC_READ,
+ WICDecodeMetadataCacheOnLoad,
+ &decoder
+ );
+
+ if (FAILED(hr))
+ {
+ return nullptr;
+ }
+
+ return decoder;
+}
+
+CComPtr WICMetadataExtractor::GetMetadataReader(IWICBitmapDecoder* decoder)
+{
+ if (!decoder)
+ {
+ return nullptr;
+ }
+
+ CComPtr frame;
+ if (FAILED(decoder->GetFrame(0, &frame)))
+ {
+ return nullptr;
+ }
+
+ CComPtr reader;
+ frame->GetMetadataQueryReader(&reader);
+
+ return reader;
+}
+
+void WICMetadataExtractor::ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
+{
+ if (!reader)
+ return;
+
+ // Extract date/time fields
+ metadata.dateTaken = ReadDateTime(reader, EXIF_DATE_TAKEN);
+ metadata.dateDigitized = ReadDateTime(reader, EXIF_DATE_DIGITIZED);
+ metadata.dateModified = ReadDateTime(reader, EXIF_DATE_MODIFIED);
+
+ // Extract camera information
+ metadata.cameraMake = ReadString(reader, EXIF_CAMERA_MAKE);
+ metadata.cameraModel = ReadString(reader, EXIF_CAMERA_MODEL);
+ metadata.lensModel = ReadString(reader, EXIF_LENS_MODEL);
+
+ // Extract shooting parameters
+ metadata.iso = ReadInteger(reader, EXIF_ISO);
+ metadata.aperture = ReadDouble(reader, EXIF_APERTURE);
+ metadata.shutterSpeed = ReadDouble(reader, EXIF_SHUTTER_SPEED);
+ metadata.focalLength = ReadDouble(reader, EXIF_FOCAL_LENGTH);
+ metadata.exposureBias = ReadDouble(reader, EXIF_EXPOSURE_BIAS);
+ metadata.flash = ReadInteger(reader, EXIF_FLASH);
+
+ // Extract image properties
+ metadata.width = ReadInteger(reader, EXIF_WIDTH);
+ metadata.height = ReadInteger(reader, EXIF_HEIGHT);
+ metadata.orientation = ReadInteger(reader, EXIF_ORIENTATION);
+ metadata.colorSpace = ReadInteger(reader, EXIF_COLOR_SPACE);
+
+ // Extract author information
+ metadata.author = ReadString(reader, EXIF_ARTIST);
+ metadata.copyright = ReadString(reader, EXIF_COPYRIGHT);
+}
+
+void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
+{
+ if (!reader)
+ {
+ return;
+ }
+
+ auto lat = ReadMetadata(reader, GPS_LATITUDE);
+ auto lon = ReadMetadata(reader, GPS_LONGITUDE);
+ auto latRef = ReadMetadata(reader, GPS_LATITUDE_REF);
+ auto lonRef = ReadMetadata(reader, GPS_LONGITUDE_REF);
+
+ if (lat && lon)
+ {
+ PropVariantValue emptyLatRef;
+ PropVariantValue emptyLonRef;
+
+ const PROPVARIANT& latRefVar = latRef ? latRef->Get() : emptyLatRef.Get();
+ const PROPVARIANT& lonRefVar = lonRef ? lonRef->Get() : emptyLonRef.Get();
+
+ auto coords = MetadataFormatHelper::ParseGPSCoordinates(
+ lat->Get(),
+ lon->Get(),
+ latRefVar,
+ lonRefVar);
+
+ metadata.latitude = coords.first;
+ metadata.longitude = coords.second;
+ }
+
+ auto alt = ReadMetadata(reader, GPS_ALTITUDE);
+ if (alt)
+ {
+ metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get());
+ }
+}
+
+
+std::optional WICMetadataExtractor::ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path)
+{
+ auto propVar = ReadMetadata(reader, path);
+ if (!propVar)
+ {
+ return std::nullopt;
+ }
+
+ std::wstring rawValue;
+ const PROPVARIANT& variant = propVar->Get();
+
+ switch (variant.vt)
+ {
+ case VT_LPWSTR:
+ if (variant.pwszVal)
+ {
+ rawValue = variant.pwszVal;
+ }
+ break;
+ case VT_BSTR:
+ if (variant.bstrVal)
+ {
+ rawValue = variant.bstrVal;
+ }
+ break;
+ case VT_LPSTR:
+ if (variant.pszVal)
+ {
+ const int size = MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, nullptr, 0);
+ if (size > 1)
+ {
+ rawValue.resize(static_cast(size) - 1);
+ MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, &rawValue[0], size);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (rawValue.empty())
+ {
+ return std::nullopt;
+ }
+
+ const std::wstring normalized = TrimWhitespace(rawValue);
+ if (normalized.empty())
+ {
+ return std::nullopt;
+ }
+
+ if (auto exifDate = ParseExifDateTime(normalized))
+ {
+ return exifDate;
+ }
+
+ if (auto isoDate = ParseIso8601DateTime(normalized))
+ {
+ return isoDate;
+ }
+
+ return std::nullopt;
+}
+
+std::optional WICMetadataExtractor::ReadString(IWICMetadataQueryReader* reader, const std::wstring& path)
+{
+ auto propVar = ReadMetadata(reader, path);
+ if (!propVar.has_value())
+ return std::nullopt;
+
+ std::wstring result;
+ switch (propVar->Get().vt)
+ {
+ case VT_LPWSTR:
+ if (propVar->Get().pwszVal)
+ result = propVar->Get().pwszVal;
+ break;
+ case VT_BSTR:
+ if (propVar->Get().bstrVal)
+ result = propVar->Get().bstrVal;
+ break;
+ case VT_LPSTR:
+ if (propVar->Get().pszVal)
+ {
+ int size = MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, nullptr, 0);
+ if (size > 1)
+ {
+ result.resize(static_cast(size) - 1);
+ MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, &result[0], size);
+ }
+ }
+ break;
+ }
+
+
+ // Trim whitespace from both ends
+ if (!result.empty())
+ {
+ size_t start = result.find_first_not_of(L" \t\r\n");
+ size_t end = result.find_last_not_of(L" \t\r\n");
+ if (start != std::wstring::npos && end != std::wstring::npos)
+ {
+ result = result.substr(start, end - start + 1);
+ }
+ else if (start == std::wstring::npos)
+ {
+ result.clear();
+ }
+ }
+
+ return result.empty() ? std::nullopt : std::make_optional(result);
+}
+
+std::optional WICMetadataExtractor::ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path)
+{
+ auto propVar = ReadMetadata(reader, path);
+ if (!propVar.has_value())
+ return std::nullopt;
+
+ int64_t result = 0;
+ switch (propVar->Get().vt)
+ {
+ case VT_I1: result = propVar->Get().cVal; break;
+ case VT_I2: result = propVar->Get().iVal; break;
+ case VT_I4: result = propVar->Get().lVal; break;
+ case VT_I8: result = propVar->Get().hVal.QuadPart; break;
+ case VT_UI1: result = propVar->Get().bVal; break;
+ case VT_UI2: result = propVar->Get().uiVal; break;
+ case VT_UI4: result = propVar->Get().ulVal; break;
+ case VT_UI8: result = static_cast(propVar->Get().uhVal.QuadPart); break;
+ default:
+ return std::nullopt;
+ }
+
+ return result;
+}
+
+std::optional WICMetadataExtractor::ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path)
+{
+ auto propVar = ReadMetadata(reader, path);
+ if (!propVar.has_value())
+ return std::nullopt;
+
+ double result = 0.0;
+ switch (propVar->Get().vt)
+ {
+ case VT_R4:
+ result = static_cast(propVar->Get().fltVal);
+ break;
+ case VT_R8:
+ result = propVar->Get().dblVal;
+ break;
+ case VT_UI1 | VT_VECTOR:
+ case VT_UI4 | VT_VECTOR:
+ // Handle rational number (common for EXIF values)
+ // Rational data is stored as 8 bytes: 4-byte numerator + 4-byte denominator
+ if (propVar->Get().caub.cElems >= 8)
+ {
+ // ExposureBias (EXIF tag 37380) uses SRATIONAL type (signed rational)
+ // which can represent negative values like -0.33 EV for exposure compensation.
+ // Most other EXIF fields use RATIONAL type (unsigned) for values like aperture, shutter speed.
+ if (path == EXIF_EXPOSURE_BIAS)
+ {
+ // Parse as signed rational: int32_t / int32_t
+ result = MetadataFormatHelper::ParseSingleSRational(propVar->Get().caub.pElems, 0);
+ break;
+ }
+ else
+ {
+ // Parse as unsigned rational: uint32_t / uint32_t
+ // First check if denominator is valid (non-zero) to avoid division by zero
+ const uint8_t* bytes = propVar->Get().caub.pElems;
+ uint32_t denominator = static_cast(bytes[4]) |
+ (static_cast(bytes[5]) << 8) |
+ (static_cast(bytes[6]) << 16) |
+ (static_cast(bytes[7]) << 24);
+
+ if (denominator != 0)
+ {
+ result = MetadataFormatHelper::ParseSingleRational(propVar->Get().caub.pElems, 0);
+ break;
+ }
+ }
+ }
+ return std::nullopt;
+ default:
+ // Try integer conversion
+ switch (propVar->Get().vt)
+ {
+ case VT_I1: result = static_cast(propVar->Get().cVal); break;
+ case VT_I2: result = static_cast(propVar->Get().iVal); break;
+ case VT_I4: result = static_cast(propVar->Get().lVal); break;
+ case VT_I8:
+ {
+ // ExposureBias (EXIF tag 37380) may be stored as VT_I8 in some WIC implementations
+ // It represents a signed rational (SRATIONAL) packed into a 64-bit integer
+ if (path == EXIF_EXPOSURE_BIAS)
+ {
+ // Parse signed rational from int64: low 32 bits = numerator, high 32 bits = denominator
+ // Some implementations may reverse the order, so we try both
+ int32_t numerator = static_cast(propVar->Get().hVal.QuadPart & 0xFFFFFFFF);
+ int32_t denominator = static_cast(propVar->Get().hVal.QuadPart >> 32);
+ if (denominator != 0)
+ {
+ result = static_cast(numerator) / static_cast(denominator);
+ }
+ else
+ {
+ // Try reversed order: high 32 bits = numerator, low 32 bits = denominator
+ numerator = static_cast(propVar->Get().hVal.QuadPart >> 32);
+ denominator = static_cast(propVar->Get().hVal.QuadPart & 0xFFFFFFFF);
+ if (denominator != 0)
+ {
+ result = static_cast(numerator) / static_cast(denominator);
+ }
+ else
+ {
+ result = 0.0; // Default to 0 if both attempts fail
+ }
+ }
+ }
+ else
+ {
+ // For other fields, treat VT_I8 as a simple 64-bit integer
+ result = static_cast(propVar->Get().hVal.QuadPart);
+ }
+ }
+ break;
+ case VT_UI1: result = static_cast(propVar->Get().bVal); break;
+ case VT_UI2: result = static_cast(propVar->Get().uiVal); break;
+ case VT_UI4: result = static_cast(propVar->Get().ulVal); break;
+ case VT_UI8:
+ {
+ // ExposureBias (EXIF tag 37380) may be stored as VT_UI8 in some WIC implementations
+ // Even though it's unsigned, we need to reinterpret it as signed for SRATIONAL
+ if (path == EXIF_EXPOSURE_BIAS)
+ {
+ // Parse signed rational from uint64 (reinterpret as signed)
+ // Low 32 bits = numerator, high 32 bits = denominator
+ int32_t numerator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
+ int32_t denominator = static_cast(propVar->Get().uhVal.QuadPart >> 32);
+ if (denominator != 0)
+ {
+ result = static_cast(numerator) / static_cast(denominator);
+ }
+ else
+ {
+ // Try reversed order: high 32 bits = numerator, low 32 bits = denominator
+ numerator = static_cast(propVar->Get().uhVal.QuadPart >> 32);
+ denominator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
+ if (denominator != 0)
+ {
+ result = static_cast(numerator) / static_cast(denominator);
+ }
+ else
+ {
+ result = 0.0; // Default to 0 if both attempts fail
+ }
+ }
+ }
+ else
+ {
+ // For other EXIF rational fields (unsigned), try both byte orders to handle different encodings
+ // First try: low 32 bits = numerator, high 32 bits = denominator
+ uint32_t numerator = static_cast