mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-07 11:46:30 +02:00
[PowerRename] Support using photo metadata to replace in the PowerRename (#41728)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request 1. Introduce WIC for power rename and add new class WICMetadataExtractor to use WIC to extract metadata. 2. Add some patterns for metadata extract. 3. Support XMP and EXIF metadata extract. 4. Add test data for xmp and exif extractor 5. Add attribution for the test data uploader. UI: <img width="2052" height="1415" alt="image" src="https://github.com/user-attachments/assets/9051b12e-4e66-4fdc-a4d4-3bada661c235" /> <img width="284" height="170" alt="image" src="https://github.com/user-attachments/assets/2fd67193-77a7-48f0-a5ac-08a69fe64e55" /> <img width="715" height="1160" alt="image" src="https://github.com/user-attachments/assets/5fa68a8c-d129-44dd-b747-099dfbcded12" /> demo: https://github.com/user-attachments/assets/e90bc206-62e5-4101-ada2-3187ee7e2039 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #5612 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Yu Leng <yuleng@microsoft.com>
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
#include "pch.h"
|
||||
#include "Helpers.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include <regex>
|
||||
#include <ShlGuid.h>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -12,6 +16,50 @@ namespace
|
||||
const int MAX_INPUT_STRING_LEN = 1024;
|
||||
|
||||
const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\PowerRename";
|
||||
|
||||
// Helper function: Find the longest matching pattern starting at the given position
|
||||
// Returns the matched pattern name, or empty string if no match found
|
||||
std::wstring FindLongestPattern(
|
||||
const std::wstring& input,
|
||||
size_t startPos,
|
||||
size_t maxPatternLength,
|
||||
const std::unordered_set<std::wstring>& validPatterns)
|
||||
{
|
||||
const size_t remaining = input.length() - startPos;
|
||||
const size_t searchLength = std::min(maxPatternLength, remaining);
|
||||
|
||||
// Try to match from longest to shortest to ensure greedy matching
|
||||
// e.g., DATE_TAKEN_YYYY should be matched before DATE_TAKEN_YY
|
||||
for (size_t len = searchLength; len > 0; --len)
|
||||
{
|
||||
std::wstring candidate = input.substr(startPos, len);
|
||||
if (validPatterns.find(candidate) != validPatterns.end())
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Helper function: Get the replacement value for a pattern
|
||||
// Returns the actual metadata value if available; if not, returns the pattern name with $ prefix
|
||||
std::wstring GetPatternValue(
|
||||
const std::wstring& patternName,
|
||||
const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
auto it = patterns.find(patternName);
|
||||
|
||||
// Return actual value if found and valid (non-empty)
|
||||
if (it != patterns.end() && !it->second.empty())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Return pattern name with $ prefix if value is unavailable
|
||||
// This provides visual feedback that the field exists but has no data
|
||||
return L"$" + patternName;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source)
|
||||
@@ -271,6 +319,72 @@ bool isFileTimeUsed(_In_ PCWSTR source)
|
||||
return used;
|
||||
}
|
||||
|
||||
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath, bool isFolder)
|
||||
{
|
||||
if (!source) return false;
|
||||
|
||||
// Early exit: If file path is provided, check file type first (fastest checks)
|
||||
// This avoids expensive pattern matching for files that don't support metadata
|
||||
if (filePath != nullptr)
|
||||
{
|
||||
// Folders don't support metadata extraction
|
||||
if (isFolder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file path is valid
|
||||
if (wcslen(filePath) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
std::wstring extension = fs::path(filePath).extension().wstring();
|
||||
|
||||
// Convert to lowercase for case-insensitive comparison
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
|
||||
|
||||
// According to the metadata support table, only these formats support metadata extraction:
|
||||
// - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
|
||||
// - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
|
||||
// - PNG (text chunks)
|
||||
static const std::unordered_set<std::wstring> supportedExtensions = {
|
||||
L".jpg",
|
||||
L".jpeg",
|
||||
L".png",
|
||||
L".tif",
|
||||
L".tiff"
|
||||
};
|
||||
|
||||
// If file type doesn't support metadata, no need to check patterns
|
||||
if (supportedExtensions.find(extension) == supportedExtensions.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now check if any metadata pattern exists in the source string
|
||||
// This is the most expensive check, so we do it last
|
||||
std::wstring str(source);
|
||||
|
||||
// Get supported patterns for the specified metadata type
|
||||
auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType);
|
||||
|
||||
// Check if any metadata pattern exists in the source string
|
||||
for (const auto& pattern : supportedPatterns)
|
||||
{
|
||||
std::wstring searchPattern = L"$" + pattern;
|
||||
if (str.find(searchPattern) != std::wstring::npos)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No metadata pattern found
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
|
||||
{
|
||||
std::locale::global(std::locale(""));
|
||||
@@ -297,10 +411,10 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
@@ -310,13 +424,13 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns
|
||||
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
@@ -326,19 +440,19 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
|
||||
formattedDate[0] = towupper(formattedDate[0]);
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
|
||||
@@ -347,31 +461,31 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff
|
||||
|
||||
StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
|
||||
res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff
|
||||
|
||||
hr = StringCchCopy(result, cchMax, res.c_str());
|
||||
}
|
||||
@@ -379,6 +493,91 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY
|
||||
return hr;
|
||||
}
|
||||
|
||||
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
if (!source || wcslen(source) == 0)
|
||||
{
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
std::wstring input(source);
|
||||
std::wstring output;
|
||||
output.reserve(input.length() * 2); // Reserve space to avoid frequent reallocations
|
||||
|
||||
// Build pattern lookup table for fast validation
|
||||
// Using all possible patterns to recognize valid pattern names even when metadata is unavailable
|
||||
auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns();
|
||||
std::unordered_set<std::wstring> validPatterns;
|
||||
validPatterns.reserve(allPatterns.size());
|
||||
size_t maxPatternLength = 0;
|
||||
for (const auto& pattern : allPatterns)
|
||||
{
|
||||
validPatterns.insert(pattern);
|
||||
maxPatternLength = std::max(maxPatternLength, pattern.length());
|
||||
}
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < input.length())
|
||||
{
|
||||
// Handle regular characters
|
||||
if (input[pos] != L'$')
|
||||
{
|
||||
output += input[pos];
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count consecutive dollar signs
|
||||
size_t dollarCount = 0;
|
||||
while (pos < input.length() && input[pos] == L'$')
|
||||
{
|
||||
dollarCount++;
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Even number of dollars: all are escaped (e.g., $$ -> $, $$$$ -> $$)
|
||||
if (dollarCount % 2 == 0)
|
||||
{
|
||||
output.append(dollarCount / 2, L'$');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Odd number of dollars: pairs are escaped, last one might be a pattern prefix
|
||||
// e.g., $ -> might be pattern, $$$ -> $ + might be pattern
|
||||
size_t escapedDollars = dollarCount / 2;
|
||||
|
||||
// If no more characters, output all dollar signs
|
||||
if (pos >= input.length())
|
||||
{
|
||||
output.append(dollarCount, L'$');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to match a pattern (greedy matching for longest pattern)
|
||||
std::wstring matchedPattern = FindLongestPattern(input, pos, maxPatternLength, validPatterns);
|
||||
|
||||
if (matchedPattern.empty())
|
||||
{
|
||||
// No pattern matched, output all dollar signs
|
||||
output.append(dollarCount, L'$');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pattern matched
|
||||
output.append(escapedDollars, L'$'); // Output escaped dollars first
|
||||
|
||||
// Replace pattern with its value or keep pattern name if value unavailable
|
||||
std::wstring replacementValue = GetPatternValue(matchedPattern, patterns);
|
||||
output += replacementValue;
|
||||
|
||||
pos += matchedPattern.length();
|
||||
}
|
||||
}
|
||||
|
||||
return StringCchCopy(result, cchMax, output.c_str());
|
||||
}
|
||||
|
||||
|
||||
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items)
|
||||
{
|
||||
*items = nullptr;
|
||||
@@ -707,4 +906,4 @@ std::wstring CreateGuidStringWithoutBrackets()
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "PowerRenameInterfaces.h"
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source);
|
||||
HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags, bool isFolder);
|
||||
HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);
|
||||
HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns);
|
||||
bool isFileTimeUsed(_In_ PCWSTR source);
|
||||
bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath = nullptr, bool isFolder = false);
|
||||
bool ShellItemArrayContainsRenamableItem(_In_ IShellItemArray* shellItemArray);
|
||||
bool DataObjectContainsRenamableItem(_In_ IUnknown* dataSource);
|
||||
HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items);
|
||||
|
||||
237
src/modules/powerrename/lib/MetadataFormatHelper.cpp
Normal file
237
src/modules/powerrename/lib/MetadataFormatHelper.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include <format>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
// Formatting functions
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatAperture(double aperture)
|
||||
{
|
||||
return std::format(L"f/{:.1f}", aperture);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatShutterSpeed(double speed)
|
||||
{
|
||||
if (speed <= 0.0)
|
||||
{
|
||||
return L"0";
|
||||
}
|
||||
|
||||
if (speed >= 1.0)
|
||||
{
|
||||
return std::format(L"{:.1f}s", speed);
|
||||
}
|
||||
|
||||
const double reciprocal = std::round(1.0 / speed);
|
||||
if (reciprocal <= 1.0)
|
||||
{
|
||||
return std::format(L"{:.3f}s", speed);
|
||||
}
|
||||
|
||||
return std::format(L"1/{:.0f}s", reciprocal);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatISO(int64_t iso)
|
||||
{
|
||||
if (iso <= 0)
|
||||
{
|
||||
return L"ISO";
|
||||
}
|
||||
|
||||
return std::format(L"ISO {}", iso);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatFlash(int64_t flashValue)
|
||||
{
|
||||
switch (flashValue & 0x1)
|
||||
{
|
||||
case 0:
|
||||
return L"Flash Off";
|
||||
case 1:
|
||||
return L"Flash On";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return std::format(L"Flash 0x{:X}", static_cast<unsigned int>(flashValue));
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatCoordinate(double coord, bool isLatitude)
|
||||
{
|
||||
wchar_t direction = isLatitude ? (coord >= 0.0 ? L'N' : L'S') : (coord >= 0.0 ? L'E' : L'W');
|
||||
double absolute = std::abs(coord);
|
||||
int degrees = static_cast<int>(absolute);
|
||||
double minutes = (absolute - static_cast<double>(degrees)) * 60.0;
|
||||
|
||||
return std::format(L"{:d}°{:.2f}'{}", degrees, minutes, direction);
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::FormatSystemTime(const SYSTEMTIME& st)
|
||||
{
|
||||
return std::format(L"{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}",
|
||||
st.wYear,
|
||||
st.wMonth,
|
||||
st.wDay,
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond);
|
||||
}
|
||||
|
||||
// Parsing functions
|
||||
|
||||
double MetadataFormatHelper::ParseGPSRational(const PROPVARIANT& pv)
|
||||
{
|
||||
if ((pv.vt & VT_VECTOR) && pv.caub.cElems >= 8)
|
||||
{
|
||||
return ParseSingleRational(pv.caub.pElems, 0);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double MetadataFormatHelper::ParseSingleRational(const uint8_t* bytes, size_t offset)
|
||||
{
|
||||
// Parse a single rational number (8 bytes: numerator + denominator)
|
||||
if (!bytes)
|
||||
return 0.0;
|
||||
|
||||
// Note: Callers are responsible for ensuring the buffer is large enough.
|
||||
// This function assumes offset points to at least 8 bytes of valid data.
|
||||
// All current callers perform cElems >= required_size checks before calling.
|
||||
const uint8_t* rationalBytes = bytes + offset;
|
||||
|
||||
// Parse as little-endian uint32_t values
|
||||
uint32_t numerator = static_cast<uint32_t>(rationalBytes[0]) |
|
||||
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[3]) << 24);
|
||||
|
||||
uint32_t denominator = static_cast<uint32_t>(rationalBytes[4]) |
|
||||
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[7]) << 24);
|
||||
|
||||
if (denominator != 0)
|
||||
{
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double MetadataFormatHelper::ParseSingleSRational(const uint8_t* bytes, size_t offset)
|
||||
{
|
||||
// Parse a single signed rational number (8 bytes: signed numerator + signed denominator)
|
||||
if (!bytes)
|
||||
return 0.0;
|
||||
|
||||
// Note: Callers are responsible for ensuring the buffer is large enough.
|
||||
// This function assumes offset points to at least 8 bytes of valid data.
|
||||
// All current callers perform cElems >= required_size checks before calling.
|
||||
const uint8_t* rationalBytes = bytes + offset;
|
||||
|
||||
// Parse as little-endian int32_t values (signed)
|
||||
// First construct as unsigned, then reinterpret as signed
|
||||
uint32_t numerator_uint = static_cast<uint32_t>(rationalBytes[0]) |
|
||||
(static_cast<uint32_t>(rationalBytes[1]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[2]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[3]) << 24);
|
||||
|
||||
uint32_t denominator_uint = static_cast<uint32_t>(rationalBytes[4]) |
|
||||
(static_cast<uint32_t>(rationalBytes[5]) << 8) |
|
||||
(static_cast<uint32_t>(rationalBytes[6]) << 16) |
|
||||
(static_cast<uint32_t>(rationalBytes[7]) << 24);
|
||||
|
||||
// Reinterpret as signed
|
||||
int32_t numerator = static_cast<int32_t>(numerator_uint);
|
||||
int32_t denominator = static_cast<int32_t>(denominator_uint);
|
||||
|
||||
if (denominator != 0)
|
||||
{
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::pair<double, double> MetadataFormatHelper::ParseGPSCoordinates(
|
||||
const PROPVARIANT& latitude,
|
||||
const PROPVARIANT& longitude,
|
||||
const PROPVARIANT& latRef,
|
||||
const PROPVARIANT& lonRef)
|
||||
{
|
||||
double lat = 0.0, lon = 0.0;
|
||||
|
||||
// Parse latitude - typically stored as 3 rationals (degrees, minutes, seconds)
|
||||
if ((latitude.vt & VT_VECTOR) && latitude.caub.cElems >= 24) // 3 rationals * 8 bytes each
|
||||
{
|
||||
const uint8_t* bytes = latitude.caub.pElems;
|
||||
|
||||
// degrees, minutes, seconds (each rational is 8 bytes)
|
||||
double degrees = ParseSingleRational(bytes, 0);
|
||||
double minutes = ParseSingleRational(bytes, 8);
|
||||
double seconds = ParseSingleRational(bytes, 16);
|
||||
|
||||
lat = degrees + minutes / 60.0 + seconds / 3600.0;
|
||||
}
|
||||
|
||||
// Parse longitude
|
||||
if ((longitude.vt & VT_VECTOR) && longitude.caub.cElems >= 24)
|
||||
{
|
||||
const uint8_t* bytes = longitude.caub.pElems;
|
||||
|
||||
double degrees = ParseSingleRational(bytes, 0);
|
||||
double minutes = ParseSingleRational(bytes, 8);
|
||||
double seconds = ParseSingleRational(bytes, 16);
|
||||
|
||||
lon = degrees + minutes / 60.0 + seconds / 3600.0;
|
||||
}
|
||||
|
||||
// Apply direction references (N/S for latitude, E/W for longitude)
|
||||
if (latRef.vt == VT_LPSTR && latRef.pszVal)
|
||||
{
|
||||
if (strcmp(latRef.pszVal, "S") == 0)
|
||||
lat = -lat;
|
||||
}
|
||||
|
||||
if (lonRef.vt == VT_LPSTR && lonRef.pszVal)
|
||||
{
|
||||
if (strcmp(lonRef.pszVal, "W") == 0)
|
||||
lon = -lon;
|
||||
}
|
||||
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
std::wstring MetadataFormatHelper::SanitizeForFileName(const std::wstring& str)
|
||||
{
|
||||
// Windows illegal filename characters: < > : " / \ | ? *
|
||||
// Also control characters (0-31) and some others
|
||||
std::wstring sanitized = str;
|
||||
|
||||
// Replace illegal characters with underscore
|
||||
for (auto& ch : sanitized)
|
||||
{
|
||||
// Check for illegal characters
|
||||
if (ch == L'<' || ch == L'>' || ch == L':' || ch == L'"' ||
|
||||
ch == L'/' || ch == L'\\' || ch == L'|' || ch == L'?' || ch == L'*' ||
|
||||
ch < 32) // Control characters
|
||||
{
|
||||
ch = L'_';
|
||||
}
|
||||
}
|
||||
|
||||
// Also remove trailing dots and spaces (Windows doesn't like them at end of filename)
|
||||
while (!sanitized.empty() && (sanitized.back() == L'.' || sanitized.back() == L' '))
|
||||
{
|
||||
sanitized.pop_back();
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
117
src/modules/powerrename/lib/MetadataFormatHelper.h
Normal file
117
src/modules/powerrename/lib/MetadataFormatHelper.h
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <windows.h>
|
||||
#include <propvarutil.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for formatting and parsing metadata values
|
||||
/// Provides static utility functions for converting metadata to human-readable strings
|
||||
/// and parsing raw metadata values
|
||||
/// </summary>
|
||||
class MetadataFormatHelper
|
||||
{
|
||||
public:
|
||||
// Formatting functions - Convert metadata values to display strings
|
||||
|
||||
/// <summary>
|
||||
/// Format aperture value (f-number)
|
||||
/// </summary>
|
||||
/// <param name="aperture">Aperture value (e.g., 2.8)</param>
|
||||
/// <returns>Formatted string (e.g., "f/2.8")</returns>
|
||||
static std::wstring FormatAperture(double aperture);
|
||||
|
||||
/// <summary>
|
||||
/// Format shutter speed
|
||||
/// </summary>
|
||||
/// <param name="speed">Shutter speed in seconds</param>
|
||||
/// <returns>Formatted string (e.g., "1/100s" or "2.5s")</returns>
|
||||
static std::wstring FormatShutterSpeed(double speed);
|
||||
|
||||
/// <summary>
|
||||
/// Format ISO value
|
||||
/// </summary>
|
||||
/// <param name="iso">ISO speed value</param>
|
||||
/// <returns>Formatted string (e.g., "ISO 400")</returns>
|
||||
static std::wstring FormatISO(int64_t iso);
|
||||
|
||||
/// <summary>
|
||||
/// Format flash status
|
||||
/// </summary>
|
||||
/// <param name="flashValue">Flash value from EXIF</param>
|
||||
/// <returns>Formatted string (e.g., "Flash On" or "Flash Off")</returns>
|
||||
static std::wstring FormatFlash(int64_t flashValue);
|
||||
|
||||
/// <summary>
|
||||
/// Format GPS coordinate
|
||||
/// </summary>
|
||||
/// <param name="coord">Coordinate value in decimal degrees</param>
|
||||
/// <param name="isLatitude">true for latitude, false for longitude</param>
|
||||
/// <returns>Formatted string (e.g., "40°26.76'N")</returns>
|
||||
static std::wstring FormatCoordinate(double coord, bool isLatitude);
|
||||
|
||||
/// <summary>
|
||||
/// Format SYSTEMTIME to string
|
||||
/// </summary>
|
||||
/// <param name="st">SYSTEMTIME structure</param>
|
||||
/// <returns>Formatted string (e.g., "2024-03-15 14:30:45")</returns>
|
||||
static std::wstring FormatSystemTime(const SYSTEMTIME& st);
|
||||
|
||||
// Parsing functions - Convert raw metadata to usable values
|
||||
|
||||
/// <summary>
|
||||
/// Parse GPS rational value from PROPVARIANT
|
||||
/// </summary>
|
||||
/// <param name="pv">PROPVARIANT containing GPS rational data</param>
|
||||
/// <returns>Parsed double value</returns>
|
||||
static double ParseGPSRational(const PROPVARIANT& pv);
|
||||
|
||||
/// <summary>
|
||||
/// Parse single rational value from byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte array containing rational data</param>
|
||||
/// <param name="offset">Offset in the byte array</param>
|
||||
/// <returns>Parsed double value (numerator / denominator)</returns>
|
||||
static double ParseSingleRational(const uint8_t* bytes, size_t offset);
|
||||
|
||||
/// <summary>
|
||||
/// Parse single signed rational value from byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte array containing signed rational data</param>
|
||||
/// <param name="offset">Offset in the byte array</param>
|
||||
/// <returns>Parsed double value (signed numerator / signed denominator)</returns>
|
||||
static double ParseSingleSRational(const uint8_t* bytes, size_t offset);
|
||||
|
||||
/// <summary>
|
||||
/// Parse GPS coordinates from PROPVARIANT values
|
||||
/// </summary>
|
||||
/// <param name="latitude">PROPVARIANT containing latitude</param>
|
||||
/// <param name="longitude">PROPVARIANT containing longitude</param>
|
||||
/// <param name="latRef">PROPVARIANT containing latitude reference (N/S)</param>
|
||||
/// <param name="lonRef">PROPVARIANT containing longitude reference (E/W)</param>
|
||||
/// <returns>Pair of (latitude, longitude) in decimal degrees</returns>
|
||||
static std::pair<double, double> ParseGPSCoordinates(
|
||||
const PROPVARIANT& latitude,
|
||||
const PROPVARIANT& longitude,
|
||||
const PROPVARIANT& latRef,
|
||||
const PROPVARIANT& lonRef);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize a string to make it safe for use in filenames
|
||||
/// Replaces illegal filename characters (< > : " / \ | ? * and control chars) with underscore
|
||||
/// Also removes trailing dots and spaces which Windows doesn't allow at end of filename
|
||||
///
|
||||
/// IMPORTANT: This should ONLY be called in ExtractPatterns to avoid performance waste.
|
||||
/// Do NOT call this function when reading raw metadata values.
|
||||
/// </summary>
|
||||
/// <param name="str">String to sanitize</param>
|
||||
/// <returns>Sanitized string safe for use in filename</returns>
|
||||
static std::wstring SanitizeForFileName(const std::wstring& str);
|
||||
};
|
||||
}
|
||||
353
src/modules/powerrename/lib/MetadataPatternExtractor.cpp
Normal file
353
src/modules/powerrename/lib/MetadataPatternExtractor.cpp
Normal file
@@ -0,0 +1,353 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "MetadataFormatHelper.h"
|
||||
#include "WICMetadataExtractor.h"
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
MetadataPatternExtractor::MetadataPatternExtractor()
|
||||
: extractor(std::make_unique<WICMetadataExtractor>())
|
||||
{
|
||||
}
|
||||
|
||||
MetadataPatternExtractor::~MetadataPatternExtractor() = default;
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractPatterns(
|
||||
const std::wstring& filePath,
|
||||
MetadataType type)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MetadataType::EXIF:
|
||||
patterns = ExtractEXIFPatterns(filePath);
|
||||
break;
|
||||
case MetadataType::XMP:
|
||||
patterns = ExtractXMPPatterns(filePath);
|
||||
break;
|
||||
default:
|
||||
return MetadataPatternMap();
|
||||
}
|
||||
|
||||
// Sanitize all pattern values for filename safety before returning
|
||||
// This ensures all metadata values are safe to use in filenames (removes illegal chars like <>:"/\|?*)
|
||||
// IMPORTANT: Only call SanitizeForFileName here to avoid performance waste
|
||||
for (auto& [key, value] : patterns)
|
||||
{
|
||||
value = MetadataFormatHelper::SanitizeForFileName(value);
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
void MetadataPatternExtractor::ClearCache()
|
||||
{
|
||||
if (extractor)
|
||||
{
|
||||
extractor->ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractEXIFPatterns(const std::wstring& filePath)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
EXIFMetadata exif;
|
||||
if (!extractor->ExtractEXIFMetadata(filePath, exif))
|
||||
{
|
||||
return patterns;
|
||||
}
|
||||
|
||||
if (exif.cameraMake.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CAMERA_MAKE] = exif.cameraMake.value();
|
||||
}
|
||||
|
||||
if (exif.cameraModel.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CAMERA_MODEL] = exif.cameraModel.value();
|
||||
}
|
||||
|
||||
if (exif.lensModel.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LENS] = exif.lensModel.value();
|
||||
}
|
||||
|
||||
if (exif.iso.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ISO] = MetadataFormatHelper::FormatISO(exif.iso.value());
|
||||
}
|
||||
|
||||
if (exif.aperture.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::APERTURE] = MetadataFormatHelper::FormatAperture(exif.aperture.value());
|
||||
}
|
||||
|
||||
if (exif.shutterSpeed.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::SHUTTER] = MetadataFormatHelper::FormatShutterSpeed(exif.shutterSpeed.value());
|
||||
}
|
||||
|
||||
if (exif.focalLength.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::FOCAL] = std::to_wstring(static_cast<int>(exif.focalLength.value())) + L"mm";
|
||||
}
|
||||
|
||||
if (exif.flash.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::FLASH] = MetadataFormatHelper::FormatFlash(exif.flash.value());
|
||||
}
|
||||
|
||||
if (exif.width.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::WIDTH] = std::to_wstring(exif.width.value());
|
||||
}
|
||||
|
||||
if (exif.height.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::HEIGHT] = std::to_wstring(exif.height.value());
|
||||
}
|
||||
|
||||
if (exif.author.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::AUTHOR] = exif.author.value();
|
||||
}
|
||||
|
||||
if (exif.copyright.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::COPYRIGHT] = exif.copyright.value();
|
||||
}
|
||||
|
||||
if (exif.latitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LATITUDE] = MetadataFormatHelper::FormatCoordinate(exif.latitude.value(), true);
|
||||
}
|
||||
|
||||
if (exif.longitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::LONGITUDE] = MetadataFormatHelper::FormatCoordinate(exif.longitude.value(), false);
|
||||
}
|
||||
|
||||
// Only extract DATE_TAKEN patterns (most commonly used)
|
||||
if (exif.dateTaken.has_value())
|
||||
{
|
||||
const SYSTEMTIME& date = exif.dateTaken.value();
|
||||
patterns[MetadataPatterns::DATE_TAKEN_YYYY] = std::format(L"{:04d}", date.wYear);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_YY] = std::format(L"{:02d}", date.wYear % 100);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_MM] = std::format(L"{:02d}", date.wMonth);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_DD] = std::format(L"{:02d}", date.wDay);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_HH] = std::format(L"{:02d}", date.wHour);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_mm] = std::format(L"{:02d}", date.wMinute);
|
||||
patterns[MetadataPatterns::DATE_TAKEN_SS] = std::format(L"{:02d}", date.wSecond);
|
||||
}
|
||||
// Note: dateDigitized and dateModified are still extracted but not exposed as patterns
|
||||
|
||||
if (exif.exposureBias.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::EXPOSURE_BIAS] = std::format(L"{:.2f}", exif.exposureBias.value());
|
||||
}
|
||||
|
||||
if (exif.orientation.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ORIENTATION] = std::to_wstring(exif.orientation.value());
|
||||
}
|
||||
|
||||
if (exif.colorSpace.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::COLOR_SPACE] = std::to_wstring(exif.colorSpace.value());
|
||||
}
|
||||
|
||||
if (exif.altitude.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ALTITUDE] = std::format(L"{:.2f} m", exif.altitude.value());
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
MetadataPatternMap MetadataPatternExtractor::ExtractXMPPatterns(const std::wstring& filePath)
|
||||
{
|
||||
MetadataPatternMap patterns;
|
||||
|
||||
XMPMetadata xmp;
|
||||
if (!extractor->ExtractXMPMetadata(filePath, xmp))
|
||||
{
|
||||
return patterns;
|
||||
}
|
||||
|
||||
if (xmp.creator.has_value())
|
||||
{
|
||||
const auto& creator = xmp.creator.value();
|
||||
patterns[MetadataPatterns::AUTHOR] = creator;
|
||||
patterns[MetadataPatterns::CREATOR] = creator;
|
||||
}
|
||||
|
||||
if (xmp.rights.has_value())
|
||||
{
|
||||
const auto& rights = xmp.rights.value();
|
||||
patterns[MetadataPatterns::RIGHTS] = rights;
|
||||
patterns[MetadataPatterns::COPYRIGHT] = rights;
|
||||
}
|
||||
|
||||
if (xmp.title.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::TITLE] = xmp.title.value();
|
||||
}
|
||||
|
||||
if (xmp.description.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::DESCRIPTION] = xmp.description.value();
|
||||
}
|
||||
|
||||
if (xmp.subject.has_value())
|
||||
{
|
||||
std::wstring joined;
|
||||
for (const auto& entry : xmp.subject.value())
|
||||
{
|
||||
if (!joined.empty())
|
||||
{
|
||||
joined.append(L"; ");
|
||||
}
|
||||
joined.append(entry);
|
||||
}
|
||||
if (!joined.empty())
|
||||
{
|
||||
patterns[MetadataPatterns::SUBJECT] = joined;
|
||||
}
|
||||
}
|
||||
|
||||
if (xmp.creatorTool.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::CREATOR_TOOL] = xmp.creatorTool.value();
|
||||
}
|
||||
|
||||
if (xmp.documentID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::DOCUMENT_ID] = xmp.documentID.value();
|
||||
}
|
||||
|
||||
if (xmp.instanceID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::INSTANCE_ID] = xmp.instanceID.value();
|
||||
}
|
||||
|
||||
if (xmp.originalDocumentID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::ORIGINAL_DOCUMENT_ID] = xmp.originalDocumentID.value();
|
||||
}
|
||||
|
||||
if (xmp.versionID.has_value())
|
||||
{
|
||||
patterns[MetadataPatterns::VERSION_ID] = xmp.versionID.value();
|
||||
}
|
||||
|
||||
// Only extract CREATE_DATE patterns (primary creation time)
|
||||
if (xmp.createDate.has_value())
|
||||
{
|
||||
const SYSTEMTIME& date = xmp.createDate.value();
|
||||
patterns[MetadataPatterns::CREATE_DATE_YYYY] = std::format(L"{:04d}", date.wYear);
|
||||
patterns[MetadataPatterns::CREATE_DATE_YY] = std::format(L"{:02d}", date.wYear % 100);
|
||||
patterns[MetadataPatterns::CREATE_DATE_MM] = std::format(L"{:02d}", date.wMonth);
|
||||
patterns[MetadataPatterns::CREATE_DATE_DD] = std::format(L"{:02d}", date.wDay);
|
||||
patterns[MetadataPatterns::CREATE_DATE_HH] = std::format(L"{:02d}", date.wHour);
|
||||
patterns[MetadataPatterns::CREATE_DATE_mm] = std::format(L"{:02d}", date.wMinute);
|
||||
patterns[MetadataPatterns::CREATE_DATE_SS] = std::format(L"{:02d}", date.wSecond);
|
||||
}
|
||||
// Note: modifyDate and metadataDate are still extracted but not exposed as patterns
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
// AddDatePatterns function has been removed as dynamic patterns are no longer supported.
|
||||
// Date patterns are now directly added inline for DATE_TAKEN and CREATE_DATE only.
|
||||
// Formatting functions have been moved to MetadataFormatHelper for better testability.
|
||||
|
||||
std::vector<std::wstring> MetadataPatternExtractor::GetSupportedPatterns(MetadataType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MetadataType::EXIF:
|
||||
return {
|
||||
MetadataPatterns::CAMERA_MAKE,
|
||||
MetadataPatterns::CAMERA_MODEL,
|
||||
MetadataPatterns::LENS,
|
||||
MetadataPatterns::ISO,
|
||||
MetadataPatterns::APERTURE,
|
||||
MetadataPatterns::SHUTTER,
|
||||
MetadataPatterns::FOCAL,
|
||||
MetadataPatterns::FLASH,
|
||||
MetadataPatterns::WIDTH,
|
||||
MetadataPatterns::HEIGHT,
|
||||
MetadataPatterns::AUTHOR,
|
||||
MetadataPatterns::COPYRIGHT,
|
||||
MetadataPatterns::LATITUDE,
|
||||
MetadataPatterns::LONGITUDE,
|
||||
MetadataPatterns::DATE_TAKEN_YYYY,
|
||||
MetadataPatterns::DATE_TAKEN_YY,
|
||||
MetadataPatterns::DATE_TAKEN_MM,
|
||||
MetadataPatterns::DATE_TAKEN_DD,
|
||||
MetadataPatterns::DATE_TAKEN_HH,
|
||||
MetadataPatterns::DATE_TAKEN_mm,
|
||||
MetadataPatterns::DATE_TAKEN_SS,
|
||||
MetadataPatterns::EXPOSURE_BIAS,
|
||||
MetadataPatterns::ORIENTATION,
|
||||
MetadataPatterns::COLOR_SPACE,
|
||||
MetadataPatterns::ALTITUDE
|
||||
};
|
||||
|
||||
case MetadataType::XMP:
|
||||
return {
|
||||
MetadataPatterns::AUTHOR,
|
||||
MetadataPatterns::COPYRIGHT,
|
||||
MetadataPatterns::RIGHTS,
|
||||
MetadataPatterns::TITLE,
|
||||
MetadataPatterns::DESCRIPTION,
|
||||
MetadataPatterns::SUBJECT,
|
||||
MetadataPatterns::CREATOR,
|
||||
MetadataPatterns::CREATOR_TOOL,
|
||||
MetadataPatterns::DOCUMENT_ID,
|
||||
MetadataPatterns::INSTANCE_ID,
|
||||
MetadataPatterns::ORIGINAL_DOCUMENT_ID,
|
||||
MetadataPatterns::VERSION_ID,
|
||||
MetadataPatterns::CREATE_DATE_YYYY,
|
||||
MetadataPatterns::CREATE_DATE_YY,
|
||||
MetadataPatterns::CREATE_DATE_MM,
|
||||
MetadataPatterns::CREATE_DATE_DD,
|
||||
MetadataPatterns::CREATE_DATE_HH,
|
||||
MetadataPatterns::CREATE_DATE_mm,
|
||||
MetadataPatterns::CREATE_DATE_SS
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::wstring> MetadataPatternExtractor::GetAllPossiblePatterns()
|
||||
{
|
||||
auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF);
|
||||
auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP);
|
||||
|
||||
std::vector<std::wstring> allPatterns;
|
||||
allPatterns.reserve(exifPatterns.size() + xmpPatterns.size());
|
||||
|
||||
allPatterns.insert(allPatterns.end(), exifPatterns.begin(), exifPatterns.end());
|
||||
allPatterns.insert(allPatterns.end(), xmpPatterns.begin(), xmpPatterns.end());
|
||||
|
||||
std::sort(allPatterns.begin(), allPatterns.end());
|
||||
allPatterns.erase(std::unique(allPatterns.begin(), allPatterns.end()), allPatterns.end());
|
||||
|
||||
return allPatterns;
|
||||
}
|
||||
|
||||
39
src/modules/powerrename/lib/MetadataPatternExtractor.h
Normal file
39
src/modules/powerrename/lib/MetadataPatternExtractor.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "MetadataTypes.h"
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
// Pattern-Value mapping for metadata replacement
|
||||
using MetadataPatternMap = std::unordered_map<std::wstring, std::wstring>;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata pattern extractor that converts metadata into replaceable patterns
|
||||
/// </summary>
|
||||
class MetadataPatternExtractor
|
||||
{
|
||||
public:
|
||||
MetadataPatternExtractor();
|
||||
~MetadataPatternExtractor();
|
||||
|
||||
MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type);
|
||||
|
||||
void ClearCache();
|
||||
|
||||
static std::vector<std::wstring> GetSupportedPatterns(MetadataType type);
|
||||
static std::vector<std::wstring> GetAllPossiblePatterns();
|
||||
|
||||
private:
|
||||
std::unique_ptr<class WICMetadataExtractor> extractor;
|
||||
|
||||
MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
|
||||
MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
|
||||
};
|
||||
}
|
||||
87
src/modules/powerrename/lib/MetadataResultCache.cpp
Normal file
87
src/modules/powerrename/lib/MetadataResultCache.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MetadataResultCache.h"
|
||||
|
||||
using namespace PowerRenameLib;
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename Metadata, typename CacheEntry, typename Cache, typename Mutex, typename Loader>
|
||||
bool GetOrLoadInternal(const std::wstring& filePath,
|
||||
Metadata& outMetadata,
|
||||
Cache& cache,
|
||||
Mutex& mutex,
|
||||
const Loader& loader)
|
||||
{
|
||||
{
|
||||
std::shared_lock sharedLock(mutex);
|
||||
auto it = cache.find(filePath);
|
||||
if (it != cache.end())
|
||||
{
|
||||
// Return cached result (success or failure)
|
||||
outMetadata = it->second.data;
|
||||
return it->second.wasSuccessful;
|
||||
}
|
||||
}
|
||||
|
||||
if (!loader)
|
||||
{
|
||||
// No loader provided
|
||||
return false;
|
||||
}
|
||||
|
||||
Metadata loaded{};
|
||||
const bool result = loader(loaded);
|
||||
|
||||
// Cache the result (success or failure)
|
||||
{
|
||||
std::unique_lock uniqueLock(mutex);
|
||||
// Check if another thread cached it while we were loading
|
||||
auto it = cache.find(filePath);
|
||||
if (it == cache.end())
|
||||
{
|
||||
// Not cached yet, insert our result
|
||||
cache.emplace(filePath, CacheEntry{ result, loaded });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Another thread cached it, use their result
|
||||
outMetadata = it->second.data;
|
||||
return it->second.wasSuccessful;
|
||||
}
|
||||
}
|
||||
|
||||
outMetadata = loaded;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool MetadataResultCache::GetOrLoadEXIF(const std::wstring& filePath,
|
||||
EXIFMetadata& outMetadata,
|
||||
const EXIFLoader& loader)
|
||||
{
|
||||
return GetOrLoadInternal<EXIFMetadata, CacheEntry<EXIFMetadata>>(filePath, outMetadata, exifCache, exifMutex, loader);
|
||||
}
|
||||
|
||||
bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath,
|
||||
XMPMetadata& outMetadata,
|
||||
const XMPLoader& loader)
|
||||
{
|
||||
return GetOrLoadInternal<XMPMetadata, CacheEntry<XMPMetadata>>(filePath, outMetadata, xmpCache, xmpMutex, loader);
|
||||
}
|
||||
|
||||
void MetadataResultCache::ClearAll()
|
||||
{
|
||||
{
|
||||
std::unique_lock lock(exifMutex);
|
||||
exifCache.clear();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock(xmpMutex);
|
||||
xmpCache.clear();
|
||||
}
|
||||
}
|
||||
39
src/modules/powerrename/lib/MetadataResultCache.h
Normal file
39
src/modules/powerrename/lib/MetadataResultCache.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
#include "MetadataTypes.h"
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
class MetadataResultCache
|
||||
{
|
||||
public:
|
||||
using EXIFLoader = std::function<bool(EXIFMetadata&)>;
|
||||
using XMPLoader = std::function<bool(XMPMetadata&)>;
|
||||
|
||||
bool GetOrLoadEXIF(const std::wstring& filePath, EXIFMetadata& outMetadata, const EXIFLoader& loader);
|
||||
bool GetOrLoadXMP(const std::wstring& filePath, XMPMetadata& outMetadata, const XMPLoader& loader);
|
||||
|
||||
void ClearAll();
|
||||
|
||||
private:
|
||||
// Wrapper to cache both success and failure states
|
||||
template<typename T>
|
||||
struct CacheEntry
|
||||
{
|
||||
bool wasSuccessful;
|
||||
T data;
|
||||
};
|
||||
|
||||
mutable std::shared_mutex exifMutex;
|
||||
mutable std::shared_mutex xmpMutex;
|
||||
std::unordered_map<std::wstring, CacheEntry<EXIFMetadata>> exifCache;
|
||||
std::unordered_map<std::wstring, CacheEntry<XMPMetadata>> xmpCache;
|
||||
};
|
||||
}
|
||||
156
src/modules/powerrename/lib/MetadataTypes.h
Normal file
156
src/modules/powerrename/lib/MetadataTypes.h
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported metadata format types
|
||||
/// </summary>
|
||||
enum class MetadataType
|
||||
{
|
||||
EXIF, // EXIF metadata (camera settings, date taken, etc.)
|
||||
XMP // XMP metadata (Dublin Core, Photoshop, etc.)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Complete EXIF metadata structure
|
||||
/// Contains all commonly used EXIF fields with optional values
|
||||
/// </summary>
|
||||
struct EXIFMetadata
|
||||
{
|
||||
// Date and time information
|
||||
std::optional<SYSTEMTIME> dateTaken; // DateTimeOriginal
|
||||
std::optional<SYSTEMTIME> dateDigitized; // DateTimeDigitized
|
||||
std::optional<SYSTEMTIME> dateModified; // DateTime
|
||||
|
||||
// Camera information
|
||||
std::optional<std::wstring> cameraMake; // Make
|
||||
std::optional<std::wstring> cameraModel; // Model
|
||||
std::optional<std::wstring> lensModel; // LensModel
|
||||
|
||||
// Shooting parameters
|
||||
std::optional<int64_t> iso; // ISO speed
|
||||
std::optional<double> aperture; // F-number
|
||||
std::optional<double> shutterSpeed; // Exposure time
|
||||
std::optional<double> focalLength; // Focal length in mm
|
||||
std::optional<double> exposureBias; // Exposure bias value
|
||||
std::optional<int64_t> flash; // Flash status
|
||||
|
||||
// Image properties
|
||||
std::optional<int64_t> width; // Image width in pixels
|
||||
std::optional<int64_t> height; // Image height in pixels
|
||||
std::optional<int64_t> orientation; // Image orientation
|
||||
std::optional<int64_t> colorSpace; // Color space
|
||||
|
||||
// Author and copyright
|
||||
std::optional<std::wstring> author; // Artist
|
||||
std::optional<std::wstring> copyright; // Copyright notice
|
||||
|
||||
// GPS information
|
||||
std::optional<double> latitude; // GPS latitude in decimal degrees
|
||||
std::optional<double> longitude; // GPS longitude in decimal degrees
|
||||
std::optional<double> altitude; // GPS altitude in meters
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// XMP (Extensible Metadata Platform) metadata structure
|
||||
/// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields
|
||||
/// </summary>
|
||||
struct XMPMetadata
|
||||
{
|
||||
// XMP Basic schema - https://ns.adobe.com/xap/1.0/
|
||||
std::optional<SYSTEMTIME> createDate; // xmp:CreateDate
|
||||
std::optional<SYSTEMTIME> modifyDate; // xmp:ModifyDate
|
||||
std::optional<SYSTEMTIME> metadataDate; // xmp:MetadataDate
|
||||
std::optional<std::wstring> creatorTool; // xmp:CreatorTool
|
||||
|
||||
// Dublin Core schema - http://purl.org/dc/elements/1.1/
|
||||
std::optional<std::wstring> title; // dc:title
|
||||
std::optional<std::wstring> description; // dc:description
|
||||
std::optional<std::wstring> creator; // dc:creator (author)
|
||||
std::optional<std::vector<std::wstring>> subject; // dc:subject (keywords)
|
||||
|
||||
// XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/
|
||||
std::optional<std::wstring> rights; // xmpRights:WebStatement (copyright)
|
||||
|
||||
// XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/
|
||||
std::optional<std::wstring> documentID; // xmpMM:DocumentID
|
||||
std::optional<std::wstring> instanceID; // xmpMM:InstanceID
|
||||
std::optional<std::wstring> originalDocumentID; // xmpMM:OriginalDocumentID
|
||||
std::optional<std::wstring> versionID; // xmpMM:VersionID
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Constants for metadata pattern names
|
||||
/// </summary>
|
||||
namespace MetadataPatterns
|
||||
{
|
||||
// EXIF patterns
|
||||
constexpr wchar_t CAMERA_MAKE[] = L"CAMERA_MAKE";
|
||||
constexpr wchar_t CAMERA_MODEL[] = L"CAMERA_MODEL";
|
||||
constexpr wchar_t LENS[] = L"LENS";
|
||||
constexpr wchar_t ISO[] = L"ISO";
|
||||
constexpr wchar_t APERTURE[] = L"APERTURE";
|
||||
constexpr wchar_t SHUTTER[] = L"SHUTTER";
|
||||
constexpr wchar_t FOCAL[] = L"FOCAL";
|
||||
constexpr wchar_t FLASH[] = L"FLASH";
|
||||
constexpr wchar_t WIDTH[] = L"WIDTH";
|
||||
constexpr wchar_t HEIGHT[] = L"HEIGHT";
|
||||
constexpr wchar_t AUTHOR[] = L"AUTHOR";
|
||||
constexpr wchar_t COPYRIGHT[] = L"COPYRIGHT";
|
||||
constexpr wchar_t LATITUDE[] = L"LATITUDE";
|
||||
constexpr wchar_t LONGITUDE[] = L"LONGITUDE";
|
||||
|
||||
// Date components from EXIF DateTimeOriginal (when photo was taken)
|
||||
constexpr wchar_t DATE_TAKEN_YYYY[] = L"DATE_TAKEN_YYYY";
|
||||
constexpr wchar_t DATE_TAKEN_YY[] = L"DATE_TAKEN_YY";
|
||||
constexpr wchar_t DATE_TAKEN_MM[] = L"DATE_TAKEN_MM";
|
||||
constexpr wchar_t DATE_TAKEN_DD[] = L"DATE_TAKEN_DD";
|
||||
constexpr wchar_t DATE_TAKEN_HH[] = L"DATE_TAKEN_HH";
|
||||
constexpr wchar_t DATE_TAKEN_mm[] = L"DATE_TAKEN_mm";
|
||||
constexpr wchar_t DATE_TAKEN_SS[] = L"DATE_TAKEN_SS";
|
||||
|
||||
// Additional EXIF patterns
|
||||
constexpr wchar_t EXPOSURE_BIAS[] = L"EXPOSURE_BIAS";
|
||||
constexpr wchar_t ORIENTATION[] = L"ORIENTATION";
|
||||
constexpr wchar_t COLOR_SPACE[] = L"COLOR_SPACE";
|
||||
constexpr wchar_t ALTITUDE[] = L"ALTITUDE";
|
||||
|
||||
// XMP patterns
|
||||
constexpr wchar_t CREATOR_TOOL[] = L"CREATOR_TOOL";
|
||||
|
||||
// Date components from XMP CreateDate
|
||||
constexpr wchar_t CREATE_DATE_YYYY[] = L"CREATE_DATE_YYYY";
|
||||
constexpr wchar_t CREATE_DATE_YY[] = L"CREATE_DATE_YY";
|
||||
constexpr wchar_t CREATE_DATE_MM[] = L"CREATE_DATE_MM";
|
||||
constexpr wchar_t CREATE_DATE_DD[] = L"CREATE_DATE_DD";
|
||||
constexpr wchar_t CREATE_DATE_HH[] = L"CREATE_DATE_HH";
|
||||
constexpr wchar_t CREATE_DATE_mm[] = L"CREATE_DATE_mm";
|
||||
constexpr wchar_t CREATE_DATE_SS[] = L"CREATE_DATE_SS";
|
||||
|
||||
// Dublin Core patterns
|
||||
constexpr wchar_t TITLE[] = L"TITLE";
|
||||
constexpr wchar_t DESCRIPTION[] = L"DESCRIPTION";
|
||||
constexpr wchar_t CREATOR[] = L"CREATOR";
|
||||
constexpr wchar_t SUBJECT[] = L"SUBJECT"; // Keywords
|
||||
|
||||
// XMP Rights pattern
|
||||
constexpr wchar_t RIGHTS[] = L"RIGHTS"; // Copyright
|
||||
|
||||
// XMP Media Management patterns
|
||||
constexpr wchar_t DOCUMENT_ID[] = L"DOCUMENT_ID";
|
||||
constexpr wchar_t INSTANCE_ID[] = L"INSTANCE_ID";
|
||||
constexpr wchar_t ORIGINAL_DOCUMENT_ID[] = L"ORIGINAL_DOCUMENT_ID";
|
||||
constexpr wchar_t VERSION_ID[] = L"VERSION_ID";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -86,6 +86,13 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
|
||||
HANDLE hFile = CreateFileW(m_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
if (hFile != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Use RAII-style scope guard to ensure handle is always closed
|
||||
struct FileHandleCloser
|
||||
{
|
||||
HANDLE handle;
|
||||
~FileHandleCloser() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); }
|
||||
} scopedHandle{ hFile };
|
||||
|
||||
FILETIME FileTime;
|
||||
bool success = false;
|
||||
|
||||
@@ -122,8 +129,6 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
*time = m_time;
|
||||
return hr;
|
||||
|
||||
@@ -16,19 +16,24 @@
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutDir>
|
||||
<DepsPath>$(ProjectDir)..\..\..\..\deps</DepsPath>
|
||||
</PropertyGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<PreprocessorDefinitions>WIN32;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)..\;$(ProjectDir)..\ui;$(ProjectDir)..\dll;$(ProjectDir)..\lib;$(ProjectDir)..\..\..\;$(ProjectDir)..\..\..\common\Telemetry;%(AdditionalIncludeDirectories);$(GeneratedFilesDir)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>/FS %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Enumerating.h" />
|
||||
@@ -47,6 +52,12 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="MetadataTypes.h" />
|
||||
<ClInclude Include="PropVariantValue.h" />
|
||||
<ClInclude Include="WICMetadataExtractor.h" />
|
||||
<ClInclude Include="MetadataPatternExtractor.h" />
|
||||
<ClInclude Include="MetadataFormatHelper.h" />
|
||||
<ClInclude Include="MetadataResultCache.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Enumerating.cpp" />
|
||||
@@ -64,6 +75,10 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="trace.cpp" />
|
||||
<ClCompile Include="WICMetadataExtractor.cpp" />
|
||||
<ClCompile Include="MetadataPatternExtractor.cpp" />
|
||||
<ClCompile Include="MetadataFormatHelper.cpp" />
|
||||
<ClCompile Include="MetadataResultCache.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -328,6 +328,22 @@ IFACEMETHODIMP CPowerRenameRegEx::ResetFileTime()
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CPowerRenameRegEx::PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns)
|
||||
{
|
||||
m_metadataPatterns = patterns;
|
||||
m_useMetadata = true;
|
||||
_OnMetadataChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP CPowerRenameRegEx::ResetMetadata()
|
||||
{
|
||||
m_metadataPatterns.clear();
|
||||
m_useMetadata = false;
|
||||
_OnMetadataChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx)
|
||||
{
|
||||
*renameRegEx = nullptr;
|
||||
@@ -387,10 +403,39 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
// TODO: creating the regex could be costly. May want to cache this.
|
||||
wchar_t newReplaceTerm[MAX_PATH] = { 0 };
|
||||
bool fileTimeErrorOccurred = false;
|
||||
bool metadataErrorOccurred = false;
|
||||
bool appliedTemplateTransform = false;
|
||||
|
||||
std::wstring replaceTemplate;
|
||||
if (m_replaceTerm)
|
||||
{
|
||||
replaceTemplate = m_replaceTerm;
|
||||
}
|
||||
|
||||
if (m_useFileTime)
|
||||
{
|
||||
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_fileTime)))
|
||||
if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_fileTime)))
|
||||
{
|
||||
fileTimeErrorOccurred = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceTemplate.assign(newReplaceTerm);
|
||||
appliedTemplateTransform = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_useMetadata)
|
||||
{
|
||||
if (FAILED(GetMetadataFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_metadataPatterns)))
|
||||
{
|
||||
metadataErrorOccurred = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceTemplate.assign(newReplaceTerm);
|
||||
appliedTemplateTransform = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring sourceToUse;
|
||||
@@ -399,9 +444,9 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
|
||||
|
||||
std::wstring searchTerm(m_searchTerm);
|
||||
std::wstring replaceTerm;
|
||||
if (m_useFileTime && !fileTimeErrorOccurred)
|
||||
if (appliedTemplateTransform)
|
||||
{
|
||||
replaceTerm = newReplaceTerm;
|
||||
replaceTerm = replaceTemplate;
|
||||
}
|
||||
else if (m_replaceTerm)
|
||||
{
|
||||
@@ -606,3 +651,43 @@ void CPowerRenameRegEx::_OnFileTimeChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CPowerRenameRegEx::_OnMetadataChanged()
|
||||
{
|
||||
CSRWSharedAutoLock lock(&m_lockEvents);
|
||||
|
||||
for (auto it : m_renameRegExEvents)
|
||||
{
|
||||
if (it.pEvents)
|
||||
{
|
||||
it.pEvents->OnMetadataChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() const
|
||||
{
|
||||
if (m_flags & MetadataSourceXMP)
|
||||
return PowerRenameLib::MetadataType::XMP;
|
||||
|
||||
// Default to EXIF
|
||||
return PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
|
||||
// Interface method implementation
|
||||
IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType)
|
||||
{
|
||||
if (metadataType == nullptr)
|
||||
return E_POINTER;
|
||||
|
||||
*metadataType = _GetMetadataTypeFromFlags();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Convenience method for internal use
|
||||
PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const
|
||||
{
|
||||
return _GetMetadataTypeFromFlags();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
62
src/modules/powerrename/lib/PropVariantValue.h
Normal file
62
src/modules/powerrename/lib/PropVariantValue.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <propvarutil.h>
|
||||
#include <propidl.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// RAII wrapper around PROPVARIANT to ensure proper initialization and cleanup.
|
||||
/// Move-only semantics keep ownership simple while still allowing use in optionals.
|
||||
/// </summary>
|
||||
struct PropVariantValue
|
||||
{
|
||||
PropVariantValue() noexcept
|
||||
{
|
||||
PropVariantInit(&value);
|
||||
}
|
||||
|
||||
~PropVariantValue()
|
||||
{
|
||||
PropVariantClear(&value);
|
||||
}
|
||||
|
||||
PropVariantValue(const PropVariantValue&) = delete;
|
||||
PropVariantValue& operator=(const PropVariantValue&) = delete;
|
||||
|
||||
PropVariantValue(PropVariantValue&& other) noexcept
|
||||
{
|
||||
value = other.value;
|
||||
PropVariantInit(&other.value); // Properly clear the moved-from object
|
||||
}
|
||||
|
||||
PropVariantValue& operator=(PropVariantValue&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
PropVariantClear(&value);
|
||||
value = other.value;
|
||||
PropVariantInit(&other.value); // Properly clear the moved-from object
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
PROPVARIANT* GetAddressOf() noexcept
|
||||
{
|
||||
return &value;
|
||||
}
|
||||
|
||||
PROPVARIANT& Get() noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
const PROPVARIANT& Get() const noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
PROPVARIANT value;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "pch.h"
|
||||
#include <winrt/base.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "Renaming.h"
|
||||
#include <Helpers.h>
|
||||
|
||||
#include "MetadataPatternExtractor.h"
|
||||
#include "PowerRenameRegEx.h"
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnumIndex, CComPtr<IPowerRenameItem>& spItem)
|
||||
@@ -14,6 +18,7 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
|
||||
PWSTR replaceTerm = nullptr;
|
||||
bool useFileTime = false;
|
||||
bool useMetadata = false;
|
||||
|
||||
winrt::check_hresult(spRenameRegEx->GetReplaceTerm(&replaceTerm));
|
||||
|
||||
@@ -21,7 +26,6 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
{
|
||||
useFileTime = true;
|
||||
}
|
||||
CoTaskMemFree(replaceTerm);
|
||||
|
||||
int id = -1;
|
||||
winrt::check_hresult(spItem->GetId(&id));
|
||||
@@ -30,6 +34,29 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
bool isSubFolderContent = false;
|
||||
winrt::check_hresult(spItem->GetIsFolder(&isFolder));
|
||||
winrt::check_hresult(spItem->GetIsSubFolderContent(&isSubFolderContent));
|
||||
|
||||
// Get metadata type to check if metadata patterns are used
|
||||
PowerRenameLib::MetadataType metadataType;
|
||||
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
// Fallback to default metadata type if call fails
|
||||
metadataType = PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
|
||||
// Check if metadata is used AND if this file type supports metadata
|
||||
// Get file path early for metadata type checking and reuse later
|
||||
PWSTR filePath = nullptr;
|
||||
winrt::check_hresult(spItem->GetPath(&filePath));
|
||||
std::wstring filePathStr(filePath); // Copy once for reuse
|
||||
CoTaskMemFree(filePath); // Free immediately after copying
|
||||
|
||||
if (isMetadataUsed(replaceTerm, metadataType, filePathStr.c_str(), isFolder))
|
||||
{
|
||||
useMetadata = true;
|
||||
}
|
||||
|
||||
CoTaskMemFree(replaceTerm);
|
||||
if ((isFolder && (flags & PowerRenameFlags::ExcludeFolders)) ||
|
||||
(!isFolder && (flags & PowerRenameFlags::ExcludeFiles)) ||
|
||||
(isSubFolderContent && (flags & PowerRenameFlags::ExcludeSubfolders)) ||
|
||||
@@ -82,6 +109,53 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
winrt::check_hresult(spRenameRegEx->PutFileTime(fileTime));
|
||||
}
|
||||
|
||||
if (useMetadata)
|
||||
{
|
||||
// Extract metadata patterns from the file
|
||||
// Note: filePathStr was already obtained and saved earlier for reuse
|
||||
|
||||
// Get metadata type using the interface method
|
||||
PowerRenameLib::MetadataType metadataType;
|
||||
HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
// Fallback to default metadata type if call fails
|
||||
metadataType = PowerRenameLib::MetadataType::EXIF;
|
||||
}
|
||||
// Extract all patterns for the selected metadata type
|
||||
// At this point we know the file is a supported image format (jpg/jpeg/png/tif/tiff)
|
||||
static std::mutex s_metadataMutex; // Mutex to protect static variables
|
||||
static std::once_flag s_metadataExtractorInitFlag;
|
||||
static std::shared_ptr<PowerRenameLib::MetadataPatternExtractor> s_metadataExtractor;
|
||||
static std::optional<PowerRenameLib::MetadataType> s_activeMetadataType;
|
||||
|
||||
// Initialize the extractor only once
|
||||
std::call_once(s_metadataExtractorInitFlag, []() {
|
||||
s_metadataExtractor = std::make_shared<PowerRenameLib::MetadataPatternExtractor>();
|
||||
});
|
||||
|
||||
// Protect access to shared state
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_metadataMutex);
|
||||
|
||||
// Clear cache if metadata type has changed
|
||||
if (s_activeMetadataType.has_value() && s_activeMetadataType.value() != metadataType)
|
||||
{
|
||||
s_metadataExtractor->ClearCache();
|
||||
}
|
||||
|
||||
// Update the active metadata type
|
||||
s_activeMetadataType = metadataType;
|
||||
}
|
||||
|
||||
// Extract patterns (this can be done outside the lock if ExtractPatterns is thread-safe)
|
||||
PowerRenameLib::MetadataPatternMap patterns = s_metadataExtractor->ExtractPatterns(filePathStr, metadataType);
|
||||
|
||||
// Always call PutMetadataPatterns to ensure all patterns get replaced
|
||||
// Even if empty, this keeps metadata placeholders consistent when no values are extracted
|
||||
winrt::check_hresult(spRenameRegEx->PutMetadataPatterns(patterns));
|
||||
}
|
||||
|
||||
PWSTR newName = nullptr;
|
||||
|
||||
// Failure here means we didn't match anything or had nothing to match
|
||||
@@ -93,6 +167,10 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
winrt::check_hresult(spRenameRegEx->ResetFileTime());
|
||||
}
|
||||
|
||||
if (useMetadata)
|
||||
{
|
||||
winrt::check_hresult(spRenameRegEx->ResetMetadata());
|
||||
}
|
||||
wchar_t resultName[MAX_PATH] = { 0 };
|
||||
|
||||
PWSTR newNameToUse = nullptr;
|
||||
@@ -206,4 +284,4 @@ bool DoRename(CComPtr<IPowerRenameRegEx>& spRenameRegEx, unsigned long& itemEnum
|
||||
CoTaskMemFree(originalName);
|
||||
|
||||
return wouldRename;
|
||||
}
|
||||
}
|
||||
|
||||
1021
src/modules/powerrename/lib/WICMetadataExtractor.cpp
Normal file
1021
src/modules/powerrename/lib/WICMetadataExtractor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
64
src/modules/powerrename/lib/WICMetadataExtractor.h
Normal file
64
src/modules/powerrename/lib/WICMetadataExtractor.h
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
#include "MetadataTypes.h"
|
||||
#include "MetadataResultCache.h"
|
||||
#include "PropVariantValue.h"
|
||||
#include <wincodec.h>
|
||||
#include <atlbase.h>
|
||||
|
||||
namespace PowerRenameLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows Imaging Component (WIC) implementation for metadata extraction
|
||||
/// Provides efficient batch extraction of all metadata types with built-in caching
|
||||
/// </summary>
|
||||
class WICMetadataExtractor
|
||||
{
|
||||
public:
|
||||
WICMetadataExtractor();
|
||||
~WICMetadataExtractor();
|
||||
|
||||
// Public metadata extraction methods
|
||||
bool ExtractEXIFMetadata(
|
||||
const std::wstring& filePath,
|
||||
EXIFMetadata& outMetadata);
|
||||
|
||||
bool ExtractXMPMetadata(
|
||||
const std::wstring& filePath,
|
||||
XMPMetadata& outMetadata);
|
||||
|
||||
void ClearCache();
|
||||
|
||||
private:
|
||||
// WIC factory management
|
||||
static CComPtr<IWICImagingFactory> GetWICFactory();
|
||||
static void InitializeWIC();
|
||||
|
||||
// WIC operations
|
||||
CComPtr<IWICBitmapDecoder> CreateDecoder(const std::wstring& filePath);
|
||||
CComPtr<IWICMetadataQueryReader> GetMetadataReader(IWICBitmapDecoder* decoder);
|
||||
|
||||
bool LoadEXIFMetadata(const std::wstring& filePath, EXIFMetadata& outMetadata);
|
||||
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
|
||||
|
||||
// Batch extraction methods
|
||||
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
||||
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
||||
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
|
||||
|
||||
// Field reading helpers
|
||||
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<int64_t> ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
std::optional<double> ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
|
||||
// Helper methods
|
||||
std::optional<PropVariantValue> ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||
|
||||
private:
|
||||
MetadataResultCache cache;
|
||||
};
|
||||
}
|
||||
@@ -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