mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-11 05:39:45 +01:00
Compare commits
2 Commits
async-cpp-
...
yuleng/ren
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0266e2b19f | ||
|
|
0efe393b0a |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
81
src/modules/powerrename/lib/CachedWICMetadataExtractor.cpp
Normal file
81
src/modules/powerrename/lib/CachedWICMetadataExtractor.cpp
Normal 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();
|
||||
}
|
||||
42
src/modules/powerrename/lib/CachedWICMetadataExtractor.h
Normal file
42
src/modules/powerrename/lib/CachedWICMetadataExtractor.h
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
51
src/modules/powerrename/lib/IMetadataExtractor.h
Normal file
51
src/modules/powerrename/lib/IMetadataExtractor.h
Normal 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;
|
||||
};
|
||||
}
|
||||
422
src/modules/powerrename/lib/MetadataPatternExtractor.cpp
Normal file
422
src/modules/powerrename/lib/MetadataPatternExtractor.cpp
Normal 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);
|
||||
}
|
||||
95
src/modules/powerrename/lib/MetadataPatternExtractor.h
Normal file
95
src/modules/powerrename/lib/MetadataPatternExtractor.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
214
src/modules/powerrename/lib/MetadataTypes.h
Normal file
214
src/modules/powerrename/lib/MetadataTypes.h
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
984
src/modules/powerrename/lib/WICMetadataExtractor.cpp
Normal file
984
src/modules/powerrename/lib/WICMetadataExtractor.cpp
Normal 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);
|
||||
}
|
||||
73
src/modules/powerrename/lib/WICMetadataExtractor.h
Normal file
73
src/modules/powerrename/lib/WICMetadataExtractor.h
Normal 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);
|
||||
};
|
||||
}
|
||||
228
src/modules/powerrename/lib/WICObjectCache.cpp
Normal file
228
src/modules/powerrename/lib/WICObjectCache.cpp
Normal 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() };
|
||||
}
|
||||
90
src/modules/powerrename/lib/WICObjectCache.h
Normal file
90
src/modules/powerrename/lib/WICObjectCache.h
Normal 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};
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user