Compare commits

...

2 Commits

Author SHA1 Message Date
Yu Leng
0266e2b19f Fix format issue 2025-09-12 15:00:09 +08:00
Yu Leng
0efe393b0a init 2025-09-12 14:52:14 +08:00
29 changed files with 3147 additions and 37 deletions

View File

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

View File

@@ -238,6 +238,7 @@ void App::OnLaunched(LaunchActivatedEventArgs const&)
#else
#define BUFSIZE 4096 * 4
g_files.push_back(L"D:\\testdata");
BOOL bSuccess;
WCHAR chBuf[BUFSIZE];
DWORD dwRead;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,81 @@
// 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 "CachedWICMetadataExtractor.h"
using namespace PowerRenameLib;
CachedWICMetadataExtractor::CachedWICMetadataExtractor()
: cache(WICObjectCache::Instance())
{
}
ExtractionResult CachedWICMetadataExtractor::ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata)
{
// Use cached metadata reader
auto reader = cache.GetMetadataReader(filePath);
if (!reader)
{
return ExtractionResult::FileNotFound;
}
// Call base class implementation with the cached reader
// Note: We need to modify the base class to support this
// For now, use the standard implementation
return WICMetadataExtractor::ExtractEXIFMetadata(filePath, outMetadata);
}
ExtractionResult CachedWICMetadataExtractor::ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata)
{
// Use cached metadata reader
auto reader = cache.GetMetadataReader(filePath);
if (!reader)
{
return ExtractionResult::FileNotFound;
}
// Call base class implementation
return WICMetadataExtractor::ExtractXMPMetadata(filePath, outMetadata);
}
CComPtr<IWICBitmapDecoder> CachedWICMetadataExtractor::CreateDecoder(const std::wstring& filePath)
{
// Use cached decoder instead of creating new one
return cache.GetDecoder(filePath);
}
CComPtr<IWICMetadataQueryReader> CachedWICMetadataExtractor::GetMetadataReader(IWICBitmapDecoder* decoder)
{
// This still needs to get the reader from the decoder
// but the decoder itself is cached
if (!decoder)
return nullptr;
CComPtr<IWICBitmapFrameDecode> frame;
HRESULT hr = decoder->GetFrame(0, &frame);
if (FAILED(hr) || !frame)
return nullptr;
CComPtr<IWICMetadataQueryReader> reader;
hr = frame->GetMetadataQueryReader(&reader);
if (FAILED(hr))
return nullptr;
return reader;
}
void CachedWICMetadataExtractor::ClearCache()
{
cache.Clear();
}
WICObjectCache::CacheStats CachedWICMetadataExtractor::GetCacheStats() const
{
return cache.GetStats();
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include "WICMetadataExtractor.h"
#include "WICObjectCache.h"
namespace PowerRenameLib
{
/// <summary>
/// Cached version of WICMetadataExtractor that reuses WIC objects
/// for improved performance when processing multiple files
/// </summary>
class CachedWICMetadataExtractor : public WICMetadataExtractor
{
public:
CachedWICMetadataExtractor();
~CachedWICMetadataExtractor() override = default;
// Override to use cached decoder
ExtractionResult ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata) override;
ExtractionResult ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata) override;
// Cache management
void ClearCache();
WICObjectCache::CacheStats GetCacheStats() const;
protected:
// Helper methods to use cached objects
CComPtr<IWICBitmapDecoder> CreateDecoder(const std::wstring& filePath);
CComPtr<IWICMetadataQueryReader> GetMetadataReader(IWICBitmapDecoder* decoder);
private:
WICObjectCache& cache;
};
}

View File

@@ -1,9 +1,11 @@
#include "pch.h"
#include "Helpers.h"
#include "MetadataTypes.h"
#include <regex>
#include <ShlGuid.h>
#include <cstring>
#include <filesystem>
#include <unordered_map>
namespace fs = std::filesystem;
@@ -271,6 +273,28 @@ bool isFileTimeUsed(_In_ PCWSTR source)
return used;
}
bool isMetadataUsed(_In_ PCWSTR source)
{
if (!source) return false;
std::wstring str(source);
// Get all possible metadata patterns from the extractor
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
// Check if any metadata pattern exists in the source string
for (const auto& pattern : allPatterns)
{
std::wstring searchPattern = L"$" + pattern;
if (str.find(searchPattern) != std::wstring::npos)
{
return true;
}
}
return false;
}
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
{
std::locale::global(std::locale(""));
@@ -379,6 +403,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)
{
HRESULT hr = E_INVALIDARG;
if (source && wcslen(source) > 0)
{
std::wstring res(source);
std::wstring output;
output.reserve(res.length() * 2); // Reserve space to avoid frequent reallocations
// Get all possible patterns to check
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
size_t i = 0;
while (i < res.length())
{
if (res[i] == L'$')
{
// Count consecutive $ symbols
size_t dollarCount = 0;
size_t start = i;
while (i < res.length() && res[i] == L'$')
{
dollarCount++;
i++;
}
bool patternFound = false;
// If we have an odd number of $, the last one might start a pattern
if (dollarCount % 2 == 1 && i < res.length())
{
// Check for matching pattern
for (const auto& pattern : allPatterns)
{
if (i + pattern.length() <= res.length() &&
res.substr(i, pattern.length()) == pattern)
{
// Add the escaped $ symbols (dollarCount - 1) / 2 pairs
for (size_t j = 0; j < (dollarCount - 1) / 2; j++)
{
output += L'$';
}
// Replace the pattern
auto it = patterns.find(pattern);
if (it != patterns.end() && !it->second.empty())
{
output += it->second;
}
else if (it != patterns.end() && it->second == L"unsupported")
{
output += L"unsupported";
}
else
{
output += L"unknown";
}
i += pattern.length();
patternFound = true;
break;
}
}
}
if (!patternFound)
{
// No pattern found, add all $ symbols as-is
output.append(dollarCount, L'$');
}
}
else
{
output += res[i];
i++;
}
}
hr = StringCchCopy(result, cchMax, output.c_str());
}
return hr;
}
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items)
{
*items = nullptr;

View File

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

View File

@@ -0,0 +1,51 @@
// 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 <string>
#include <memory>
namespace PowerRenameLib
{
/// <summary>
/// Interface for metadata extraction implementations
/// Allows for dependency injection and unit testing
/// </summary>
class IMetadataExtractor
{
public:
virtual ~IMetadataExtractor() = default;
/// <summary>
/// Extract EXIF metadata from an image file
/// </summary>
/// <param name="filePath">Path to the image file</param>
/// <param name="outMetadata">Output metadata structure</param>
/// <returns>Extraction result status</returns>
virtual ExtractionResult ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata) = 0;
/// <summary>
/// Extract XMP metadata from an image file
/// </summary>
/// <param name="filePath">Path to the image file</param>
/// <param name="outMetadata">Output metadata structure</param>
/// <returns>Extraction result status</returns>
virtual ExtractionResult ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata) = 0;
/// <summary>
/// Check if a file is supported for metadata extraction
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <param name="metadataType">Type of metadata to check</param>
/// <returns>True if the file format and metadata type are supported</returns>
virtual bool IsSupported(
const std::wstring& filePath,
MetadataType metadataType) = 0;
};
}

View File

@@ -0,0 +1,422 @@
// 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 "WICMetadataExtractor.h"
#include "CachedWICMetadataExtractor.h"
#include <format>
#include <sstream>
#include <iomanip>
#include <cmath>
#include <thread>
#include <future>
using namespace PowerRenameLib;
MetadataPatternExtractor::MetadataPatternExtractor(
std::shared_ptr<IMetadataExtractor> extractor)
: extractor(extractor ? extractor : GetDefaultExtractor())
{
}
std::shared_ptr<IMetadataExtractor> MetadataPatternExtractor::GetDefaultExtractor()
{
static auto defaultExtractor = std::make_shared<CachedWICMetadataExtractor>();
return defaultExtractor;
}
MetadataPatternMap MetadataPatternExtractor::ExtractPatterns(
const std::wstring& filePath,
MetadataType type)
{
switch (type)
{
case MetadataType::EXIF:
return ExtractEXIFPatterns(filePath);
case MetadataType::XMP:
return ExtractXMPPatterns(filePath);
default:
return MetadataPatternMap();
}
}
std::vector<std::pair<std::wstring, MetadataPatternMap>> MetadataPatternExtractor::ExtractPatternsFromFiles(
const std::vector<std::wstring>& filePaths,
MetadataType type)
{
std::vector<std::future<std::pair<std::wstring, MetadataPatternMap>>> futures;
// Launch async tasks for each file
for (const auto& filePath : filePaths)
{
futures.push_back(std::async(std::launch::async, [this, filePath, type]() {
return std::make_pair(filePath, ExtractPatterns(filePath, type));
}));
}
// Collect results
std::vector<std::pair<std::wstring, MetadataPatternMap>> results;
for (auto& future : futures)
{
results.push_back(future.get());
}
return results;
}
bool MetadataPatternExtractor::IsSupported(const std::wstring& filePath, MetadataType type) const
{
return extractor->IsSupported(filePath, type);
}
MetadataPatternMap MetadataPatternExtractor::ExtractEXIFPatterns(const std::wstring& filePath)
{
MetadataPatternMap patterns;
if (!extractor->IsSupported(filePath, MetadataType::EXIF))
{
return patterns;
}
EXIFMetadata exif;
if (extractor->ExtractEXIFMetadata(filePath, exif) != ExtractionResult::Success)
{
return patterns;
}
// Camera information
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();
}
// Shooting parameters
if (exif.iso.has_value())
{
patterns[MetadataPatterns::ISO] = FormatISO(exif.iso.value());
}
if (exif.aperture.has_value())
{
patterns[MetadataPatterns::APERTURE] = FormatAperture(exif.aperture.value());
}
if (exif.shutterSpeed.has_value())
{
patterns[MetadataPatterns::SHUTTER] = FormatShutterSpeed(exif.shutterSpeed.value());
}
if (exif.focalLength.has_value())
{
patterns[MetadataPatterns::FOCAL] = std::to_wstring(static_cast<int>(exif.focalLength.value())) + L"mm";
}
if (exif.flash.has_value())
{
patterns[MetadataPatterns::FLASH] = FormatFlash(exif.flash.value());
}
// Image properties
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());
}
// Author and copyright
if (exif.author.has_value())
{
patterns[MetadataPatterns::AUTHOR] = exif.author.value();
}
if (exif.copyright.has_value())
{
patterns[MetadataPatterns::COPYRIGHT] = exif.copyright.value();
}
// Location
if (exif.latitude.has_value())
{
patterns[MetadataPatterns::LATITUDE] = FormatCoordinate(exif.latitude.value(), true);
}
if (exif.longitude.has_value())
{
patterns[MetadataPatterns::LONGITUDE] = FormatCoordinate(exif.longitude.value(), false);
}
// Date patterns
if (exif.dateTaken.has_value())
{
AddDatePatterns(exif.dateTaken.value(), DatePatternSuffixes::DATE_TAKEN_PREFIX, patterns);
}
return patterns;
}
MetadataPatternMap MetadataPatternExtractor::ExtractXMPPatterns(const std::wstring& filePath)
{
MetadataPatternMap patterns;
if (!extractor->IsSupported(filePath, MetadataType::XMP))
{
return patterns;
}
XMPMetadata xmp;
if (extractor->ExtractXMPMetadata(filePath, xmp) != ExtractionResult::Success)
{
return patterns;
}
// Author and copyright
if (xmp.creator.has_value())
{
patterns[MetadataPatterns::AUTHOR] = xmp.creator.value();
}
if (xmp.rights.has_value())
{
patterns[MetadataPatterns::COPYRIGHT] = xmp.rights.value();
}
if (xmp.title.has_value())
{
patterns[MetadataPatterns::TITLE] = xmp.title.value();
}
if (xmp.subject.has_value())
{
// Join keywords with semicolons
std::wstring keywords;
const auto& subjectVector = xmp.subject.value();
for (size_t i = 0; i < subjectVector.size(); ++i)
{
if (i > 0) keywords += L"; ";
keywords += subjectVector[i];
}
patterns[MetadataPatterns::SUBJECT] = keywords;
}
// Date patterns
if (xmp.createDate.has_value())
{
AddDatePatterns(xmp.createDate.value(), DatePatternSuffixes::CREATE_PREFIX, patterns);
}
if (xmp.modifyDate.has_value())
{
AddDatePatterns(xmp.modifyDate.value(), DatePatternSuffixes::MODIFY_PREFIX, patterns);
}
if (xmp.metadataDate.has_value())
{
AddDatePatterns(xmp.metadataDate.value(), DatePatternSuffixes::METADATA_PREFIX, patterns);
}
return patterns;
}
void MetadataPatternExtractor::AddDatePatterns(
const SYSTEMTIME& date,
const std::wstring& prefix,
MetadataPatternMap& patterns)
{
// Full date-time format
patterns[prefix + DatePatternSuffixes::DATE_TIME] = FormatSystemTime(date);
// Date only
patterns[prefix + DatePatternSuffixes::DATE] = std::format(L"{:04d}-{:02d}-{:02d}",
date.wYear, date.wMonth, date.wDay);
// Individual components
patterns[prefix + DatePatternSuffixes::YEAR] = std::to_wstring(date.wYear);
patterns[prefix + DatePatternSuffixes::MONTH] = std::format(L"{:02d}", date.wMonth);
patterns[prefix + DatePatternSuffixes::DAY] = std::format(L"{:02d}", date.wDay);
patterns[prefix + DatePatternSuffixes::HOUR] = std::format(L"{:02d}", date.wHour);
patterns[prefix + DatePatternSuffixes::MINUTE] = std::format(L"{:02d}", date.wMinute);
patterns[prefix + DatePatternSuffixes::SECOND] = std::format(L"{:02d}", date.wSecond);
// Month names
static const std::wstring monthNames[] = {
L"Jan", L"Feb", L"Mar", L"Apr", L"May", L"Jun",
L"Jul", L"Aug", L"Sep", L"Oct", L"Nov", L"Dec"
};
if (date.wMonth >= 1 && date.wMonth <= 12)
{
patterns[prefix + DatePatternSuffixes::MONTH_NAME] = monthNames[date.wMonth - 1];
}
}
std::wstring MetadataPatternExtractor::FormatAperture(double aperture)
{
return std::format(L"f_{:.1f}", aperture);
}
std::wstring MetadataPatternExtractor::FormatShutterSpeed(double speed)
{
if (speed < 1.0)
{
int denominator = static_cast<int>(std::round(1.0 / speed));
return std::format(L"1_{}", denominator);
}
else
{
return std::format(L"{:.1f}s", speed);
}
}
std::wstring MetadataPatternExtractor::FormatISO(int64_t iso)
{
return DatePatternSuffixes::ISO_PREFIX + std::to_wstring(iso);
}
std::wstring MetadataPatternExtractor::FormatFlash(int64_t flashValue)
{
return (flashValue & 1) ? L"Flash" : L"NoFlash";
}
std::wstring MetadataPatternExtractor::FormatCoordinate(double coord, bool isLatitude)
{
wchar_t direction = isLatitude ? (coord >= 0 ? L'N' : L'S') : (coord >= 0 ? L'E' : L'W');
double absCoord = std::abs(coord);
int degrees = static_cast<int>(absCoord);
double minutes = (absCoord - degrees) * 60.0;
return std::format(L"{:d}°{:.2f}'{}", degrees, minutes, direction);
}
std::wstring MetadataPatternExtractor::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);
}
std::vector<std::wstring> MetadataPatternExtractor::GetSupportedPatterns(MetadataType type)
{
switch (type)
{
case MetadataType::EXIF:
return {
MetadataPatterns::CAMERA_MAKE,
MetadataPatterns::CAMERA_MODEL,
MetadataPatterns::LENS,
MetadataPatterns::ISO,
MetadataPatterns::APERTURE,
MetadataPatterns::SHUTTER,
MetadataPatterns::FOCAL,
MetadataPatterns::FLASH,
MetadataPatterns::WIDTH,
MetadataPatterns::HEIGHT,
MetadataPatterns::AUTHOR,
MetadataPatterns::COPYRIGHT,
MetadataPatterns::LATITUDE,
MetadataPatterns::LONGITUDE,
MetadataPatterns::DATE_TAKEN_YYYY,
MetadataPatterns::DATE_TAKEN_YY,
MetadataPatterns::DATE_TAKEN_MM,
MetadataPatterns::DATE_TAKEN_DD,
MetadataPatterns::DATE_TAKEN_HH,
MetadataPatterns::DATE_TAKEN_mm,
MetadataPatterns::DATE_TAKEN_SS,
MetadataPatterns::EXPOSURE_BIAS,
MetadataPatterns::ORIENTATION,
MetadataPatterns::COLOR_SPACE,
MetadataPatterns::ALTITUDE,
};
case MetadataType::XMP:
return {
MetadataPatterns::AUTHOR,
MetadataPatterns::COPYRIGHT,
MetadataPatterns::RIGHTS,
MetadataPatterns::TITLE,
MetadataPatterns::DESCRIPTION,
MetadataPatterns::SUBJECT,
MetadataPatterns::CREATOR,
MetadataPatterns::CREATOR_TOOL,
MetadataPatterns::DOCUMENT_ID,
MetadataPatterns::INSTANCE_ID,
MetadataPatterns::ORIGINAL_DOCUMENT_ID,
MetadataPatterns::VERSION_ID,
MetadataPatterns::CREATE_DATE_YYYY,
MetadataPatterns::CREATE_DATE_YY,
MetadataPatterns::CREATE_DATE_MM,
MetadataPatterns::CREATE_DATE_DD,
MetadataPatterns::CREATE_DATE_HH,
MetadataPatterns::CREATE_DATE_mm,
MetadataPatterns::CREATE_DATE_SS,
MetadataPatterns::MODIFY_DATE_YYYY,
MetadataPatterns::MODIFY_DATE_YY,
MetadataPatterns::MODIFY_DATE_MM,
MetadataPatterns::MODIFY_DATE_DD,
MetadataPatterns::MODIFY_DATE_HH,
MetadataPatterns::MODIFY_DATE_mm,
MetadataPatterns::MODIFY_DATE_SS,
MetadataPatterns::METADATA_DATE_YYYY,
MetadataPatterns::METADATA_DATE_YY,
MetadataPatterns::METADATA_DATE_MM,
MetadataPatterns::METADATA_DATE_DD,
MetadataPatterns::METADATA_DATE_HH,
MetadataPatterns::METADATA_DATE_mm,
MetadataPatterns::METADATA_DATE_SS
};
default:
return {};
}
}
std::vector<std::wstring> MetadataPatternExtractor::GetAllPossiblePatterns()
{
auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF);
auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP);
std::vector<std::wstring> allPatterns;
allPatterns.reserve(exifPatterns.size() + xmpPatterns.size());
allPatterns.insert(allPatterns.end(), exifPatterns.begin(), exifPatterns.end());
allPatterns.insert(allPatterns.end(), xmpPatterns.begin(), xmpPatterns.end());
// Remove duplicates
std::sort(allPatterns.begin(), allPatterns.end());
allPatterns.erase(std::unique(allPatterns.begin(), allPatterns.end()), allPatterns.end());
return allPatterns;
}
// Static methods for backward compatibility
MetadataPatternMap MetadataPatternExtractor::ExtractPatternsStatic(
const std::wstring& filePath,
MetadataType type)
{
static auto extractor = GetDefaultExtractor();
MetadataPatternExtractor instance(extractor);
return instance.ExtractPatterns(filePath, type);
}
bool MetadataPatternExtractor::IsSupportedStatic(
const std::wstring& filePath,
MetadataType type)
{
static auto extractor = GetDefaultExtractor();
MetadataPatternExtractor instance(extractor);
return instance.IsSupported(filePath, type);
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include "MetadataTypes.h"
#include "IMetadataExtractor.h"
namespace PowerRenameLib
{
// Pattern-Value mapping for metadata replacement
using MetadataPatternMap = std::unordered_map<std::wstring, std::wstring>;
/// <summary>
/// Enhanced metadata pattern extractor with dependency injection support
/// </summary>
class MetadataPatternExtractor
{
public:
/// <summary>
/// Constructor with optional dependency injection
/// </summary>
/// <param name="extractor">Optional metadata extractor implementation</param>
explicit MetadataPatternExtractor(
std::shared_ptr<IMetadataExtractor> extractor = nullptr);
/// <summary>
/// Extract all patterns for the specified metadata type from the file
/// </summary>
MetadataPatternMap ExtractPatterns(
const std::wstring& filePath,
MetadataType type);
/// <summary>
/// Extract patterns from multiple files in parallel
/// </summary>
std::vector<std::pair<std::wstring, MetadataPatternMap>> ExtractPatternsFromFiles(
const std::vector<std::wstring>& filePaths,
MetadataType type);
/// <summary>
/// Check if the file supports the specified metadata type
/// </summary>
bool IsSupported(const std::wstring& filePath, MetadataType type) const;
/// <summary>
/// Get patterns supported by specific metadata type
/// </summary>
static std::vector<std::wstring> GetSupportedPatterns(MetadataType type);
/// <summary>
/// Get all possible metadata patterns
/// </summary>
static std::vector<std::wstring> GetAllPossiblePatterns();
// Static methods for backward compatibility
static MetadataPatternMap ExtractPatternsStatic(
const std::wstring& filePath,
MetadataType type);
static bool IsSupportedStatic(
const std::wstring& filePath,
MetadataType type);
private:
// Metadata extractor instance
std::shared_ptr<IMetadataExtractor> extractor;
// Default static extractor for backward compatibility
static std::shared_ptr<IMetadataExtractor> GetDefaultExtractor();
// Extract patterns for each metadata type
MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
// Extract date patterns from SYSTEMTIME
void AddDatePatterns(
const SYSTEMTIME& date,
const std::wstring& prefix,
MetadataPatternMap& patterns);
// Formatting helpers (static as they don't need instance data)
static std::wstring FormatAperture(double aperture);
static std::wstring FormatShutterSpeed(double speed);
static std::wstring FormatISO(int64_t iso);
static std::wstring FormatFlash(int64_t flashValue);
static std::wstring FormatCoordinate(double coord, bool isLatitude);
static std::wstring FormatSystemTime(const SYSTEMTIME& st);
};
}

View File

@@ -0,0 +1,214 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <string>
#include <optional>
#include <vector>
#include <windows.h>
namespace PowerRenameLib
{
/// <summary>
/// Supported metadata format types
/// </summary>
enum class MetadataType
{
EXIF, // EXIF metadata (camera settings, date taken, etc.)
XMP // XMP metadata (Dublin Core, Photoshop, etc.)
};
/// <summary>
/// Result of metadata extraction operations
/// </summary>
enum class ExtractionResult
{
Success, // Successfully extracted metadata
Failed, // General failure
FileNotFound, // File does not exist
UnsupportedFormat, // File format is not supported
MetadataNotFound, // No metadata found in file
DecoderError, // WIC decoder error
InvalidParameter // Invalid input parameter
};
/// <summary>
/// Complete EXIF metadata structure
/// Contains all commonly used EXIF fields with optional values
/// </summary>
struct EXIFMetadata
{
// Date and time information
std::optional<SYSTEMTIME> dateTaken; // DateTimeOriginal
std::optional<SYSTEMTIME> dateDigitized; // DateTimeDigitized
std::optional<SYSTEMTIME> dateModified; // DateTime
// Camera information
std::optional<std::wstring> cameraMake; // Make
std::optional<std::wstring> cameraModel; // Model
std::optional<std::wstring> lensModel; // LensModel
// Shooting parameters
std::optional<int64_t> iso; // ISO speed
std::optional<double> aperture; // F-number
std::optional<double> shutterSpeed; // Exposure time
std::optional<double> focalLength; // Focal length in mm
std::optional<double> exposureBias; // Exposure bias value
std::optional<int64_t> flash; // Flash status
// Image properties
std::optional<int64_t> width; // Image width in pixels
std::optional<int64_t> height; // Image height in pixels
std::optional<int64_t> orientation; // Image orientation
std::optional<int64_t> colorSpace; // Color space
// Author and copyright
std::optional<std::wstring> author; // Artist
std::optional<std::wstring> copyright; // Copyright notice
// GPS information
std::optional<double> latitude; // GPS latitude in decimal degrees
std::optional<double> longitude; // GPS longitude in decimal degrees
std::optional<double> altitude; // GPS altitude in meters
};
/// <summary>
/// XMP (Extensible Metadata Platform) metadata structure
/// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields
/// </summary>
struct XMPMetadata
{
// XMP Basic schema - https://ns.adobe.com/xap/1.0/
std::optional<SYSTEMTIME> createDate; // xmp:CreateDate
std::optional<SYSTEMTIME> modifyDate; // xmp:ModifyDate
std::optional<SYSTEMTIME> metadataDate; // xmp:MetadataDate
std::optional<std::wstring> creatorTool; // xmp:CreatorTool
// Dublin Core schema - http://purl.org/dc/elements/1.1/
std::optional<std::wstring> title; // dc:title
std::optional<std::wstring> description; // dc:description
std::optional<std::wstring> creator; // dc:creator (author)
std::optional<std::vector<std::wstring>> subject; // dc:subject (keywords)
// XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/
std::optional<std::wstring> rights; // xmpRights:WebStatement (copyright)
// XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/
std::optional<std::wstring> documentID; // xmpMM:DocumentID
std::optional<std::wstring> instanceID; // xmpMM:InstanceID
std::optional<std::wstring> originalDocumentID; // xmpMM:OriginalDocumentID
std::optional<std::wstring> versionID; // xmpMM:VersionID
};
/// <summary>
/// Constants for metadata pattern names
/// </summary>
namespace MetadataPatterns
{
// EXIF patterns
constexpr wchar_t CAMERA_MAKE[] = L"CAMERA_MAKE";
constexpr wchar_t CAMERA_MODEL[] = L"CAMERA_MODEL";
constexpr wchar_t LENS[] = L"LENS";
constexpr wchar_t ISO[] = L"ISO";
constexpr wchar_t APERTURE[] = L"APERTURE";
constexpr wchar_t SHUTTER[] = L"SHUTTER";
constexpr wchar_t FOCAL[] = L"FOCAL";
constexpr wchar_t FLASH[] = L"FLASH";
constexpr wchar_t WIDTH[] = L"WIDTH";
constexpr wchar_t HEIGHT[] = L"HEIGHT";
constexpr wchar_t AUTHOR[] = L"AUTHOR";
constexpr wchar_t COPYRIGHT[] = L"COPYRIGHT";
constexpr wchar_t LATITUDE[] = L"LATITUDE";
constexpr wchar_t LONGITUDE[] = L"LONGITUDE";
// Date components from EXIF DateTimeOriginal (when photo was taken)
constexpr wchar_t DATE_TAKEN_YYYY[] = L"DATE_TAKEN_YYYY";
constexpr wchar_t DATE_TAKEN_YY[] = L"DATE_TAKEN_YY";
constexpr wchar_t DATE_TAKEN_MM[] = L"DATE_TAKEN_MM";
constexpr wchar_t DATE_TAKEN_DD[] = L"DATE_TAKEN_DD";
constexpr wchar_t DATE_TAKEN_HH[] = L"DATE_TAKEN_HH";
constexpr wchar_t DATE_TAKEN_mm[] = L"DATE_TAKEN_mm";
constexpr wchar_t DATE_TAKEN_SS[] = L"DATE_TAKEN_SS";
// Additional EXIF patterns
constexpr wchar_t EXPOSURE_BIAS[] = L"EXPOSURE_BIAS";
constexpr wchar_t ORIENTATION[] = L"ORIENTATION";
constexpr wchar_t COLOR_SPACE[] = L"COLOR_SPACE";
constexpr wchar_t ALTITUDE[] = L"ALTITUDE";
// XMP patterns
constexpr wchar_t CREATOR_TOOL[] = L"CREATOR_TOOL";
// Date components from XMP CreateDate
constexpr wchar_t CREATE_DATE_YYYY[] = L"CREATE_DATE_YYYY";
constexpr wchar_t CREATE_DATE_YY[] = L"CREATE_DATE_YY";
constexpr wchar_t CREATE_DATE_MM[] = L"CREATE_DATE_MM";
constexpr wchar_t CREATE_DATE_DD[] = L"CREATE_DATE_DD";
constexpr wchar_t CREATE_DATE_HH[] = L"CREATE_DATE_HH";
constexpr wchar_t CREATE_DATE_mm[] = L"CREATE_DATE_mm";
constexpr wchar_t CREATE_DATE_SS[] = L"CREATE_DATE_SS";
// Date components from XMP ModifyDate
constexpr wchar_t MODIFY_DATE_YYYY[] = L"MODIFY_DATE_YYYY";
constexpr wchar_t MODIFY_DATE_YY[] = L"MODIFY_DATE_YY";
constexpr wchar_t MODIFY_DATE_MM[] = L"MODIFY_DATE_MM";
constexpr wchar_t MODIFY_DATE_DD[] = L"MODIFY_DATE_DD";
constexpr wchar_t MODIFY_DATE_HH[] = L"MODIFY_DATE_HH";
constexpr wchar_t MODIFY_DATE_mm[] = L"MODIFY_DATE_mm";
constexpr wchar_t MODIFY_DATE_SS[] = L"MODIFY_DATE_SS";
// Date components from XMP MetadataDate
constexpr wchar_t METADATA_DATE_YYYY[] = L"METADATA_DATE_YYYY";
constexpr wchar_t METADATA_DATE_YY[] = L"METADATA_DATE_YY";
constexpr wchar_t METADATA_DATE_MM[] = L"METADATA_DATE_MM";
constexpr wchar_t METADATA_DATE_DD[] = L"METADATA_DATE_DD";
constexpr wchar_t METADATA_DATE_HH[] = L"METADATA_DATE_HH";
constexpr wchar_t METADATA_DATE_mm[] = L"METADATA_DATE_mm";
constexpr wchar_t METADATA_DATE_SS[] = L"METADATA_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";
}
/// <summary>
/// Constants for date pattern suffixes and other dynamic patterns
/// </summary>
namespace DatePatternSuffixes
{
// Date pattern suffixes
constexpr wchar_t DATE_TIME[] = L"DATE_TIME";
constexpr wchar_t DATE[] = L"DATE";
constexpr wchar_t YEAR[] = L"YEAR";
constexpr wchar_t MONTH[] = L"MONTH";
constexpr wchar_t DAY[] = L"DAY";
constexpr wchar_t HOUR[] = L"HOUR";
constexpr wchar_t MINUTE[] = L"MINUTE";
constexpr wchar_t SECOND[] = L"SECOND";
constexpr wchar_t MONTH_NAME[] = L"MONTH_NAME";
// Date pattern prefixes
constexpr wchar_t DATE_TAKEN_PREFIX[] = L"DATE_TAKEN_";
constexpr wchar_t CREATE_PREFIX[] = L"CREATE_";
constexpr wchar_t MODIFY_PREFIX[] = L"MODIFY_";
constexpr wchar_t METADATA_PREFIX[] = L"METADATA_";
// Other formatting constants
constexpr wchar_t ISO_PREFIX[] = L"ISO";
}
}

View File

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

View File

@@ -74,7 +74,7 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
else
{
// Default to modification time if no specific flag is set
parsedTimeType = PowerRenameFlags::CreationTime;
parsedTimeType = PowerRenameFlags::CreationTime;
}
if (m_isTimeParsed && parsedTimeType == m_parsedTimeType)
@@ -121,9 +121,9 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
}
}
}
}
CloseHandle(hFile);
CloseHandle(hFile);
}
}
*time = m_time;
return hr;

View File

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

View File

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

View File

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

View File

@@ -329,6 +329,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;
@@ -388,11 +404,18 @@ 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;
if (m_useFileTime)
{
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_fileTime)))
fileTimeErrorOccurred = true;
}
if (m_useMetadata)
{
if (FAILED(GetMetadataFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_metadataPatterns)))
metadataErrorOccurred = true;
}
std::wstring sourceToUse;
std::wstring originalSource;
@@ -407,6 +430,10 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
{
replaceTerm = newReplaceTerm;
}
else if (m_useMetadata && !metadataErrorOccurred)
{
replaceTerm = newReplaceTerm;
}
else if (m_replaceTerm)
{
replaceTerm = m_replaceTerm;
@@ -590,3 +617,41 @@ void CPowerRenameRegEx::_OnFileTimeChanged()
}
}
}
void CPowerRenameRegEx::_OnMetadataChanged()
{
CSRWSharedAutoLock lock(&m_lockEvents);
for (auto it : m_renameRegExEvents)
{
if (it.pEvents)
{
it.pEvents->OnMetadataChanged();
}
}
}
PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() const
{
if (m_flags & MetadataSourceXMP)
return PowerRenameLib::MetadataType::XMP;
// Default to EXIF
return PowerRenameLib::MetadataType::EXIF;
}
// Interface method implementation
IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType)
{
if (metadataType == nullptr)
return E_POINTER;
*metadataType = _GetMetadataTypeFromFlags();
return S_OK;
}
// Convenience method for internal use
PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
{
return _GetMetadataTypeFromFlags();
}

View File

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

View File

@@ -3,6 +3,8 @@
#include "Renaming.h"
#include <Helpers.h>
#include "MetadataPatternExtractor.h"
#include "PowerRenameRegEx.h"
namespace fs = std::filesystem;
@@ -14,6 +16,7 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
PWSTR replaceTerm = nullptr;
bool useFileTime = false;
bool useMetadata = false;
winrt::check_hresult(spRenameRegEx->GetReplaceTerm(&replaceTerm));
@@ -21,6 +24,11 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
{
useFileTime = true;
}
if (isMetadataUsed(replaceTerm))
{
useMetadata = true;
}
CoTaskMemFree(replaceTerm);
int id = -1;
@@ -82,6 +90,33 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
winrt::check_hresult(spRenameRegEx->PutFileTime(fileTime));
}
if (useMetadata)
{
// Extract metadata patterns from the file
PWSTR filePath = nullptr;
winrt::check_hresult(spItem->GetPath(&filePath));
std::wstring filePathStr(filePath);
CoTaskMemFree(filePath);
// 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
PowerRenameLib::MetadataPatternMap patterns =
PowerRenameLib::MetadataPatternExtractor::ExtractPatternsStatic(filePathStr, metadataType);
// Always call PutMetadataPatterns to ensure all patterns get replaced
// Even if empty, this ensures unsupported patterns become "unsupported" and missing values become "unknown"
winrt::check_hresult(spRenameRegEx->PutMetadataPatterns(patterns));
}
PWSTR newName = nullptr;
// Failure here means we didn't match anything or had nothing to match
@@ -93,6 +128,10 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
winrt::check_hresult(spRenameRegEx->ResetFileTime());
}
if (useMetadata)
{
winrt::check_hresult(spRenameRegEx->ResetMetadata());
}
wchar_t resultName[MAX_PATH] = { 0 };
PWSTR newNameToUse = nullptr;

View File

@@ -0,0 +1,984 @@
// 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 <algorithm>
#include <sstream>
#include <iomanip>
#include <comdef.h>
#include <shlwapi.h>
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
// Global WIC factory management
CComPtr<IWICImagingFactory> g_wicFactory;
std::once_flag g_wicInitFlag;
}
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<LPVOID*>(&g_wicFactory)
);
if (FAILED(hr))
{
g_wicFactory = nullptr;
}
});
}
void WICMetadataExtractor::CleanupWIC()
{
g_wicFactory = nullptr;
// Don't call CoUninitialize - caller is responsible for COM lifecycle
}
CComPtr<IWICImagingFactory> WICMetadataExtractor::GetWICFactory()
{
return g_wicFactory;
}
ExtractionResult WICMetadataExtractor::ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata)
{
CComPtr<IWICMetadataQueryReader> reader;
// Check if file exists
if (!PathFileExistsW(filePath.c_str()))
{
return ExtractionResult::FileNotFound;
}
auto decoder = CreateDecoder(filePath);
if (!decoder)
{
return ExtractionResult::UnsupportedFormat;
}
// Get first frame
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
return ExtractionResult::DecoderError;
}
// Get metadata reader
reader = GetMetadataReader(decoder);
if (!reader)
{
return ExtractionResult::MetadataNotFound;
}
// Extract all EXIF fields in batch
ExtractAllEXIFFields(reader, outMetadata);
// Extract GPS data
ExtractGPSData(reader, outMetadata);
return ExtractionResult::Success;
}
bool WICMetadataExtractor::IsSupported(const std::wstring& filePath, MetadataType metadataType)
{
// First check if WIC can decode the file at all
auto decoder = CreateDecoder(filePath);
if (!decoder)
{
return false;
}
// Get metadata reader to check if specific metadata type is present
auto reader = GetMetadataReader(decoder);
if (!reader)
{
return false;
}
// Check for presence of specific metadata type based on known paths
std::wstring testPath;
switch (metadataType)
{
case MetadataType::EXIF:
// Test for common EXIF paths
testPath = L"/app1/ifd/exif/";
break;
case MetadataType::XMP:
// Test for XMP namespace
testPath = L"/xmp/";
break;
default:
return false;
}
// Try to query for the metadata type - if it exists, we support it
PROPVARIANT propValue;
PropVariantInit(&propValue);
HRESULT hr = reader->GetMetadataByName(testPath.c_str(), &propValue);
PropVariantClear(&propValue);
// For both XMP and EXIF, we should return true if the file can be decoded
// since extraction might work even if the test path doesn't exist
// (different image formats may use different metadata paths)
if (metadataType == MetadataType::XMP || metadataType == MetadataType::EXIF)
{
return true; // If we can decode the file, we can try extraction
}
// For other metadata types, check if the query succeeded
return SUCCEEDED(hr);
}
CComPtr<IWICBitmapDecoder> WICMetadataExtractor::CreateDecoder(const std::wstring& filePath)
{
auto factory = GetWICFactory();
if (!factory)
{
return nullptr;
}
CComPtr<IWICBitmapDecoder> decoder;
HRESULT hr = factory->CreateDecoderFromFilename(
filePath.c_str(),
nullptr,
GENERIC_READ,
WICDecodeMetadataCacheOnLoad,
&decoder
);
if (FAILED(hr))
{
return nullptr;
}
return decoder;
}
CComPtr<IWICMetadataQueryReader> WICMetadataExtractor::GetMetadataReader(IWICBitmapDecoder* decoder)
{
if (!decoder)
{
return nullptr;
}
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
return nullptr;
}
CComPtr<IWICMetadataQueryReader> 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;
// Extract GPS coordinates
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.has_value() && lon.has_value())
{
auto coords = ParseGPSCoordinates(lat.value(), lon.value(),
latRef.value_or(PROPVARIANT{}),
lonRef.value_or(PROPVARIANT{}));
metadata.latitude = coords.first;
metadata.longitude = coords.second;
PropVariantClear(&lat.value());
PropVariantClear(&lon.value());
if (latRef.has_value()) PropVariantClear(&latRef.value());
if (lonRef.has_value()) PropVariantClear(&lonRef.value());
}
// Extract altitude
auto alt = ReadMetadata(reader, GPS_ALTITUDE);
if (alt.has_value())
{
metadata.altitude = ParseGPSRational(alt.value());
PropVariantClear(&alt.value());
}
}
std::optional<SYSTEMTIME> WICMetadataExtractor::ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar.has_value())
return std::nullopt;
// Convert PROPVARIANT to string first
std::wstring dateStr;
switch (propVar->vt)
{
case VT_LPWSTR:
if (propVar->pwszVal)
dateStr = propVar->pwszVal;
break;
case VT_BSTR:
if (propVar->bstrVal)
dateStr = propVar->bstrVal;
break;
case VT_LPSTR:
if (propVar->pszVal)
{
int size = MultiByteToWideChar(CP_UTF8, 0, propVar->pszVal, -1, nullptr, 0);
if (size > 1)
{
dateStr.resize(static_cast<size_t>(size) - 1);
MultiByteToWideChar(CP_UTF8, 0, propVar->pszVal, -1, &dateStr[0], size);
}
}
break;
}
PropVariantClear(&propVar.value());
if (dateStr.empty())
return std::nullopt;
// Parse date formats
SYSTEMTIME st = {0};
// Try EXIF date format first: "YYYY:MM:DD HH:MM:SS"
if (dateStr.length() >= 19)
{
if (swscanf_s(dateStr.c_str(), L"%hd:%hd:%hd %hd:%hd:%hd",
&st.wYear, &st.wMonth, &st.wDay,
&st.wHour, &st.wMinute, &st.wSecond) == 6)
{
if (st.wYear > 0 && st.wMonth > 0 && st.wMonth <= 12 &&
st.wDay > 0 && st.wDay <= 31)
{
return st;
}
}
}
// Try XMP ISO 8601 format: "YYYY-MM-DDTHH:MM:SS" or with timezone
if (dateStr.length() >= 19)
{
// Try basic ISO format without milliseconds
if (swscanf_s(dateStr.c_str(), L"%hd-%hd-%hdT%hd:%hd:%hd",
&st.wYear, &st.wMonth, &st.wDay,
&st.wHour, &st.wMinute, &st.wSecond) == 6)
{
if (st.wYear > 0 && st.wMonth > 0 && st.wMonth <= 12 &&
st.wDay > 0 && st.wDay <= 31)
{
return st;
}
}
}
// Try alternative ISO format with space instead of T
if (dateStr.length() >= 19)
{
if (swscanf_s(dateStr.c_str(), L"%hd-%hd-%hd %hd:%hd:%hd",
&st.wYear, &st.wMonth, &st.wDay,
&st.wHour, &st.wMinute, &st.wSecond) == 6)
{
if (st.wYear > 0 && st.wMonth > 0 && st.wMonth <= 12 &&
st.wDay > 0 && st.wDay <= 31)
{
return st;
}
}
}
return std::nullopt;
}
std::optional<std::wstring> 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->vt)
{
case VT_LPWSTR:
if (propVar->pwszVal)
result = propVar->pwszVal;
break;
case VT_BSTR:
if (propVar->bstrVal)
result = propVar->bstrVal;
break;
case VT_LPSTR:
if (propVar->pszVal)
{
int size = MultiByteToWideChar(CP_UTF8, 0, propVar->pszVal, -1, nullptr, 0);
if (size > 1)
{
result.resize(static_cast<size_t>(size) - 1);
MultiByteToWideChar(CP_UTF8, 0, propVar->pszVal, -1, &result[0], size);
}
}
break;
}
PropVariantClear(&propVar.value());
// 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();
}
}
// For XMP strings, also sanitize for file names
if (!result.empty())
{
result = SanitizeForFileName(result);
}
return result.empty() ? std::nullopt : std::make_optional(result);
}
std::wstring WICMetadataExtractor::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;
}
std::optional<int64_t> 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->vt)
{
case VT_I1: result = propVar->cVal; break;
case VT_I2: result = propVar->iVal; break;
case VT_I4: result = propVar->lVal; break;
case VT_I8: result = propVar->hVal.QuadPart; break;
case VT_UI1: result = propVar->bVal; break;
case VT_UI2: result = propVar->uiVal; break;
case VT_UI4: result = propVar->ulVal; break;
case VT_UI8: result = static_cast<int64_t>(propVar->uhVal.QuadPart); break;
default:
PropVariantClear(&propVar.value());
return std::nullopt;
}
PropVariantClear(&propVar.value());
return result;
}
std::optional<double> 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->vt)
{
case VT_R4:
result = static_cast<double>(propVar->fltVal);
break;
case VT_R8:
result = propVar->dblVal;
break;
case VT_UI1 | VT_VECTOR:
case VT_UI4 | VT_VECTOR:
// Handle rational number (common for EXIF values)
// Check if this is signed rational (SRATIONAL) for ExposureBias
if (propVar->caub.cElems >= 8)
{
// For ExposureBias and similar fields, we need signed rational
// The path contains "37380" which is ExposureBiasValue tag
if (path.find(L"37380") != std::wstring::npos)
{
result = ParseSingleSRational(propVar->caub.pElems, 0);
break;
}
else
{
// Extract denominator to check if the rational is valid
const uint8_t* bytes = propVar->caub.pElems;
uint32_t denominator = static_cast<uint32_t>(bytes[4]) |
(static_cast<uint32_t>(bytes[5]) << 8) |
(static_cast<uint32_t>(bytes[6]) << 16) |
(static_cast<uint32_t>(bytes[7]) << 24);
if (denominator != 0)
{
result = ParseSingleRational(propVar->caub.pElems, 0);
break;
}
}
}
PropVariantClear(&propVar.value());
return std::nullopt;
default:
// Try integer conversion
switch (propVar->vt)
{
case VT_I1: result = static_cast<double>(propVar->cVal); break;
case VT_I2: result = static_cast<double>(propVar->iVal); break;
case VT_I4: result = static_cast<double>(propVar->lVal); break;
case VT_I8:
{
// Check if this is ExposureBias (SRATIONAL stored as VT_I8)
if (path.find(L"37380") != std::wstring::npos)
{
// ExposureBias: signed rational stored as int64
// For EXIF SRATIONAL in WIC: low 32 bits = numerator, high 32 bits = denominator
int32_t numerator = static_cast<int32_t>(propVar->hVal.QuadPart & 0xFFFFFFFF);
int32_t denominator = static_cast<int32_t>(propVar->hVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// If denominator is 0, try the other way around
numerator = static_cast<int32_t>(propVar->hVal.QuadPart >> 32);
denominator = static_cast<int32_t>(propVar->hVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
result = 0.0; // Default to 0 for ExposureBias if can't parse
}
}
}
else
{
result = static_cast<double>(propVar->hVal.QuadPart);
}
}
break;
case VT_UI1: result = static_cast<double>(propVar->bVal); break;
case VT_UI2: result = static_cast<double>(propVar->uiVal); break;
case VT_UI4: result = static_cast<double>(propVar->ulVal); break;
case VT_UI8:
{
// Check if this is ExposureBias (SRATIONAL stored as VT_UI8)
if (path.find(L"37380") != std::wstring::npos)
{
// ExposureBias: signed rational stored as uint64 but should be interpreted as signed
// For EXIF SRATIONAL in WIC: low 32 bits = numerator, high 32 bits = denominator
int32_t numerator = static_cast<int32_t>(propVar->uhVal.QuadPart & 0xFFFFFFFF);
int32_t denominator = static_cast<int32_t>(propVar->uhVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// If denominator is 0, try the other way around
numerator = static_cast<int32_t>(propVar->uhVal.QuadPart >> 32);
denominator = static_cast<int32_t>(propVar->uhVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
result = 0.0; // Default to 0 for ExposureBias if can't parse
}
}
}
else
{
// VT_UI8 for EXIF rational: Try both orders to handle different encodings
// First try: low 32 bits = numerator, high 32 bits = denominator
uint32_t numerator = static_cast<uint32_t>(propVar->uhVal.QuadPart & 0xFFFFFFFF);
uint32_t denominator = static_cast<uint32_t>(propVar->uhVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Second try: high 32 bits = numerator, low 32 bits = denominator
numerator = static_cast<uint32_t>(propVar->uhVal.QuadPart >> 32);
denominator = static_cast<uint32_t>(propVar->uhVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Fall back to treating as regular integer if denominator is 0
result = static_cast<double>(propVar->uhVal.QuadPart);
}
}
}
}
break;
default:
PropVariantClear(&propVar.value());
return std::nullopt;
}
}
PropVariantClear(&propVar.value());
return result;
}
std::optional<PROPVARIANT> WICMetadataExtractor::ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path)
{
if (!reader)
return std::nullopt;
PROPVARIANT value;
PropVariantInit(&value);
HRESULT hr = reader->GetMetadataByName(path.c_str(), &value);
if (SUCCEEDED(hr))
{
return value;
}
return std::nullopt;
}
double WICMetadataExtractor::ParseGPSRational(const PROPVARIANT& pv)
{
if ((pv.vt & VT_VECTOR) && pv.caub.cElems >= 8)
{
return ParseSingleRational(pv.caub.pElems, 0);
}
return 0.0;
}
double WICMetadataExtractor::ParseSingleRational(const uint8_t* bytes, size_t offset)
{
// Parse a single rational number (8 bytes: numerator + denominator)
if (!bytes)
return 0.0;
const uint8_t* rationalBytes = bytes + offset;
// Parse as little-endian uint32_t values
uint32_t numerator = static_cast<uint32_t>(rationalBytes[0]) |
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
(static_cast<uint32_t>(rationalBytes[3]) << 24);
uint32_t denominator = static_cast<uint32_t>(rationalBytes[4]) |
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
(static_cast<uint32_t>(rationalBytes[7]) << 24);
if (denominator != 0)
{
return static_cast<double>(numerator) / static_cast<double>(denominator);
}
return 0.0;
}
double WICMetadataExtractor::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;
const uint8_t* rationalBytes = bytes + offset;
// Parse as little-endian int32_t values (signed)
// First construct as unsigned, then reinterpret as signed
uint32_t numerator_uint = static_cast<uint32_t>(rationalBytes[0]) |
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
(static_cast<uint32_t>(rationalBytes[3]) << 24);
uint32_t denominator_uint = static_cast<uint32_t>(rationalBytes[4]) |
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
(static_cast<uint32_t>(rationalBytes[7]) << 24);
// Reinterpret as signed
int32_t numerator = static_cast<int32_t>(numerator_uint);
int32_t denominator = static_cast<int32_t>(denominator_uint);
if (denominator != 0)
{
return static_cast<double>(numerator) / static_cast<double>(denominator);
}
return 0.0;
}
std::pair<double, double> WICMetadataExtractor::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};
}
ExtractionResult WICMetadataExtractor::ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata)
{
// Check if file exists
if (!PathFileExistsW(filePath.c_str()))
{
return ExtractionResult::FileNotFound;
}
auto decoder = CreateDecoder(filePath);
if (!decoder)
{
return ExtractionResult::UnsupportedFormat;
}
// Get first frame
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
return ExtractionResult::DecoderError;
}
// Get the root metadata reader
CComPtr<IWICMetadataQueryReader> rootReader;
if (FAILED(frame->GetMetadataQueryReader(&rootReader)))
{
return ExtractionResult::MetadataNotFound;
}
// The actual XMP data might be in a nested reader
// Based on our path enumeration, XMP fields are directly accessible from root
// using paths like "/xmp//xmp:CreatorTool"
// Extract XMP fields using the root reader
ExtractAllXMPFields(rootReader, outMetadata);
return ExtractionResult::Success;
}
// ReadStringArray helper method
std::optional<std::vector<std::wstring>> WICMetadataExtractor::ReadStringArray(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar.has_value())
return std::nullopt;
std::vector<std::wstring> result;
switch (propVar->vt)
{
case VT_VECTOR | VT_LPWSTR:
if (propVar->calpwstr.cElems > 0 && propVar->calpwstr.pElems)
{
for (ULONG i = 0; i < propVar->calpwstr.cElems; ++i)
{
if (propVar->calpwstr.pElems[i])
{
result.push_back(propVar->calpwstr.pElems[i]);
}
}
}
break;
case VT_VECTOR | VT_BSTR:
if (propVar->cabstr.cElems > 0 && propVar->cabstr.pElems)
{
for (ULONG i = 0; i < propVar->cabstr.cElems; ++i)
{
if (propVar->cabstr.pElems[i])
{
result.push_back(propVar->cabstr.pElems[i]);
}
}
}
break;
case VT_LPWSTR:
if (propVar->pwszVal)
{
result.push_back(propVar->pwszVal);
}
break;
case VT_BSTR:
if (propVar->bstrVal)
{
result.push_back(propVar->bstrVal);
}
break;
}
PropVariantClear(&propVar.value());
// Sanitize each string in the array for file names
for (auto& str : result)
{
// Trim whitespace
if (!str.empty())
{
size_t start = str.find_first_not_of(L" \t\r\n");
size_t end = str.find_last_not_of(L" \t\r\n");
if (start != std::wstring::npos && end != std::wstring::npos)
{
str = str.substr(start, end - start + 1);
}
else if (start == std::wstring::npos)
{
str.clear();
}
}
// Sanitize for file names
if (!str.empty())
{
str = SanitizeForFileName(str);
}
}
// Remove any empty strings from the result
result.erase(
std::remove_if(result.begin(), result.end(),
[](const std::wstring& s) { return s.empty(); }),
result.end());
return result.empty() ? std::nullopt : std::make_optional(result);
}
// Batch extraction method implementations
void WICMetadataExtractor::ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata)
{
if (!reader)
return;
// XMP Basic schema - xmp: namespace
metadata.creatorTool = ReadString(reader, XMP_CREATOR_TOOL);
metadata.createDate = ReadDateTime(reader, XMP_CREATE_DATE);
metadata.modifyDate = ReadDateTime(reader, XMP_MODIFY_DATE);
metadata.metadataDate = ReadDateTime(reader, XMP_METADATA_DATE);
// Dublin Core schema - dc: namespace
metadata.title = ReadString(reader, XMP_DC_TITLE);
metadata.description = ReadString(reader, XMP_DC_DESCRIPTION);
metadata.creator = ReadString(reader, XMP_DC_CREATOR);
// For dc:subject, we need to handle the array structure
// Try to read individual elements
std::vector<std::wstring> subjects;
for (int i = 0; i < 10; ++i) // Try up to 10 subjects
{
std::wstring subjectPath = L"/xmp/dc:subject/{ulong=" + std::to_wstring(i) + L"}";
auto subject = ReadString(reader, subjectPath);
if (subject.has_value())
{
subjects.push_back(subject.value());
}
else
{
break; // No more subjects
}
}
if (!subjects.empty())
{
metadata.subject = subjects;
}
// XMP Rights Management schema
metadata.rights = ReadString(reader, XMP_RIGHTS);
// XMP Media Management schema - xmpMM: namespace
metadata.documentID = ReadString(reader, XMP_MM_DOCUMENT_ID);
metadata.instanceID = ReadString(reader, XMP_MM_INSTANCE_ID);
metadata.originalDocumentID = ReadString(reader, XMP_MM_ORIGINAL_DOCUMENT_ID);
metadata.versionID = ReadString(reader, XMP_MM_VERSION_ID);
}

View File

@@ -0,0 +1,73 @@
// 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 "IMetadataExtractor.h"
#include "MetadataTypes.h"
#include <wincodec.h>
#include <atlbase.h>
namespace PowerRenameLib
{
/// <summary>
/// Windows Imaging Component (WIC) implementation for metadata extraction
/// Provides efficient batch extraction of all metadata types
/// </summary>
class WICMetadataExtractor : public IMetadataExtractor
{
public:
WICMetadataExtractor();
~WICMetadataExtractor() override;
// IMetadataExtractor implementation
ExtractionResult ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata) override;
ExtractionResult ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata) override;
bool IsSupported(const std::wstring& filePath, MetadataType metadataType) override;
private:
// WIC factory management
static CComPtr<IWICImagingFactory> GetWICFactory();
static void InitializeWIC();
static void CleanupWIC();
// WIC operations
CComPtr<IWICBitmapDecoder> CreateDecoder(const std::wstring& filePath);
CComPtr<IWICMetadataQueryReader> GetMetadataReader(IWICBitmapDecoder* decoder);
// Batch extraction methods
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
// Field reading helpers
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<int64_t> ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<double> ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<std::vector<std::wstring>> ReadStringArray(IWICMetadataQueryReader* reader, const std::wstring& path);
// GPS utilities
static double ParseGPSRational(const PROPVARIANT& pv);
static double ParseSingleRational(const uint8_t* bytes, size_t offset);
static double ParseSingleSRational(const uint8_t* bytes, size_t offset);
static std::pair<double, double> ParseGPSCoordinates(
const PROPVARIANT& latitude,
const PROPVARIANT& longitude,
const PROPVARIANT& latRef,
const PROPVARIANT& lonRef);
// Helper methods
std::optional<PROPVARIANT> ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path);
public:
// Public helper for testing and external use
static std::wstring SanitizeForFileName(const std::wstring& str);
};
}

View File

@@ -0,0 +1,228 @@
// 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 "WICObjectCache.h"
using namespace PowerRenameLib;
// Static member initialization
CComPtr<IWICImagingFactory> WICObjectCache::wicFactory;
std::once_flag WICObjectCache::factoryInitFlag;
WICObjectCache& WICObjectCache::Instance()
{
static WICObjectCache instance;
return instance;
}
void WICObjectCache::InitializeFactory()
{
if (!wicFactory)
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
HRESULT hr = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&wicFactory));
if (FAILED(hr))
{
throw std::runtime_error("Failed to create WIC factory");
}
}
}
CComPtr<IWICBitmapDecoder> WICObjectCache::GetDecoder(const std::wstring& filePath)
{
// Ensure factory is initialized
std::call_once(factoryInitFlag, InitializeFactory);
// Check cache with read lock first
{
std::shared_lock<std::shared_mutex> lock(cacheMutex);
auto it = cache.find(filePath);
if (it != cache.end())
{
// Check if entry is still valid
if (IsEntryValid(filePath, it->second.first))
{
cacheHits++;
// Update access time (requires write lock)
lock.unlock();
std::unique_lock<std::shared_mutex> writeLock(cacheMutex);
it->second.first.lastAccess = std::chrono::steady_clock::now();
UpdateLRU(filePath);
return it->second.first.decoder;
}
}
}
// Cache miss - create new decoder
cacheMisses++;
CComPtr<IWICBitmapDecoder> decoder;
HRESULT hr = wicFactory->CreateDecoderFromFilename(
filePath.c_str(),
nullptr,
GENERIC_READ,
WICDecodeMetadataCacheOnDemand,
&decoder);
if (FAILED(hr) || !decoder)
{
return nullptr;
}
// Add to cache with write lock
std::unique_lock<std::shared_mutex> lock(cacheMutex);
// Check cache size and evict if necessary
if (cache.size() >= MAX_CACHE_SIZE)
{
EvictLRU();
}
// Get file modification time
std::filesystem::file_time_type lastWriteTime;
try
{
lastWriteTime = std::filesystem::last_write_time(filePath);
}
catch (...)
{
// If we can't get the modification time, use a default
lastWriteTime = std::filesystem::file_time_type::min();
}
// Create cache entry
DecoderInfo info;
info.decoder = decoder;
info.lastWriteTime = lastWriteTime;
info.lastAccess = std::chrono::steady_clock::now();
// Add to LRU list and cache
lruList.push_front(filePath);
cache[filePath] = std::make_pair(info, lruList.begin());
return decoder;
}
CComPtr<IWICMetadataQueryReader> WICObjectCache::GetMetadataReader(const std::wstring& filePath)
{
auto decoder = GetDecoder(filePath);
if (!decoder)
{
return nullptr;
}
CComPtr<IWICBitmapFrameDecode> frame;
HRESULT hr = decoder->GetFrame(0, &frame);
if (FAILED(hr) || !frame)
{
return nullptr;
}
CComPtr<IWICMetadataQueryReader> reader;
hr = frame->GetMetadataQueryReader(&reader);
if (FAILED(hr))
{
return nullptr;
}
return reader;
}
bool WICObjectCache::IsEntryValid(const std::wstring& path, const DecoderInfo& info) const
{
// Check if file has been modified
try
{
auto currentWriteTime = std::filesystem::last_write_time(path);
if (currentWriteTime != info.lastWriteTime)
{
return false;
}
}
catch (...)
{
// If we can't check the file, assume it's invalid
return false;
}
// Check if entry has expired
auto now = std::chrono::steady_clock::now();
if (now - info.lastAccess > CACHE_EXPIRY_TIME)
{
return false;
}
return true;
}
void WICObjectCache::EvictLRU()
{
if (!lruList.empty())
{
// Remove the least recently used item
auto lruPath = lruList.back();
lruList.pop_back();
cache.erase(lruPath);
}
}
void WICObjectCache::UpdateLRU(const std::wstring& path)
{
auto it = cache.find(path);
if (it != cache.end())
{
// Move to front of LRU list
lruList.erase(it->second.second);
lruList.push_front(path);
it->second.second = lruList.begin();
}
}
void WICObjectCache::Clear()
{
std::unique_lock<std::shared_mutex> lock(cacheMutex);
cache.clear();
lruList.clear();
cacheHits = 0;
cacheMisses = 0;
}
void WICObjectCache::CleanExpired()
{
std::unique_lock<std::shared_mutex> lock(cacheMutex);
auto now = std::chrono::steady_clock::now();
std::vector<std::wstring> toRemove;
for (const auto& [path, entry] : cache)
{
if (!IsEntryValid(path, entry.first))
{
toRemove.push_back(path);
}
}
for (const auto& path : toRemove)
{
auto it = cache.find(path);
if (it != cache.end())
{
lruList.erase(it->second.second);
cache.erase(it);
}
}
}
WICObjectCache::CacheStats WICObjectCache::GetStats() const
{
std::shared_lock<std::shared_mutex> lock(cacheMutex);
return { cache.size(), cacheHits.load(), cacheMisses.load() };
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <wincodec.h>
#include <atlbase.h>
#include <string>
#include <unordered_map>
#include <shared_mutex>
#include <filesystem>
#include <chrono>
#include <list>
namespace PowerRenameLib
{
/// <summary>
/// Thread-safe cache for WIC decoder objects to improve performance
/// by avoiding repeated decoder creation for the same files
/// </summary>
class WICObjectCache
{
public:
struct DecoderInfo
{
CComPtr<IWICBitmapDecoder> decoder;
std::filesystem::file_time_type lastWriteTime;
std::chrono::steady_clock::time_point lastAccess;
};
private:
// Thread-safe cache storage
mutable std::shared_mutex cacheMutex;
// LRU cache implementation
std::list<std::wstring> lruList;
std::unordered_map<std::wstring, std::pair<DecoderInfo, std::list<std::wstring>::iterator>> cache;
// Cache configuration
static constexpr size_t MAX_CACHE_SIZE = 10;
static constexpr auto CACHE_EXPIRY_TIME = std::chrono::minutes(5);
// Singleton WIC factory
static CComPtr<IWICImagingFactory> wicFactory;
static std::once_flag factoryInitFlag;
WICObjectCache() = default;
public:
static WICObjectCache& Instance();
// Get or create decoder for a file
CComPtr<IWICBitmapDecoder> GetDecoder(const std::wstring& filePath);
// Get metadata reader from cached decoder
CComPtr<IWICMetadataQueryReader> GetMetadataReader(const std::wstring& filePath);
// Clear cache
void Clear();
// Remove expired entries
void CleanExpired();
// Get cache statistics
struct CacheStats
{
size_t size;
size_t hits;
size_t misses;
};
CacheStats GetStats() const;
private:
// Initialize WIC factory
static void InitializeFactory();
// Check if cached entry is still valid
bool IsEntryValid(const std::wstring& path, const DecoderInfo& info) const;
// Evict least recently used entry
void EvictLRU();
// Update LRU order
void UpdateLRU(const std::wstring& path);
// Cache statistics
mutable std::atomic<size_t> cacheHits{0};
mutable std::atomic<size_t> cacheMisses{0};
};
}

View File

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