[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:
moooyo
2025-11-04 09:27:16 +08:00
committed by GitHub
parent 957b653210
commit 70e1177a6a
54 changed files with 4941 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include "MetadataTypes.h"
namespace PowerRenameLib
{
// Pattern-Value mapping for metadata replacement
using MetadataPatternMap = std::unordered_map<std::wstring, std::wstring>;
/// <summary>
/// Metadata pattern extractor that converts metadata into replaceable patterns
/// </summary>
class MetadataPatternExtractor
{
public:
MetadataPatternExtractor();
~MetadataPatternExtractor();
MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type);
void ClearCache();
static std::vector<std::wstring> GetSupportedPatterns(MetadataType type);
static std::vector<std::wstring> GetAllPossiblePatterns();
private:
std::unique_ptr<class WICMetadataExtractor> extractor;
MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath);
MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath);
};
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "pch.h"
#include "MetadataResultCache.h"
using namespace PowerRenameLib;
namespace
{
template <typename Metadata, typename CacheEntry, typename Cache, typename Mutex, typename Loader>
bool GetOrLoadInternal(const std::wstring& filePath,
Metadata& outMetadata,
Cache& cache,
Mutex& mutex,
const Loader& loader)
{
{
std::shared_lock sharedLock(mutex);
auto it = cache.find(filePath);
if (it != cache.end())
{
// Return cached result (success or failure)
outMetadata = it->second.data;
return it->second.wasSuccessful;
}
}
if (!loader)
{
// No loader provided
return false;
}
Metadata loaded{};
const bool result = loader(loaded);
// Cache the result (success or failure)
{
std::unique_lock uniqueLock(mutex);
// Check if another thread cached it while we were loading
auto it = cache.find(filePath);
if (it == cache.end())
{
// Not cached yet, insert our result
cache.emplace(filePath, CacheEntry{ result, loaded });
}
else
{
// Another thread cached it, use their result
outMetadata = it->second.data;
return it->second.wasSuccessful;
}
}
outMetadata = loaded;
return result;
}
}
bool MetadataResultCache::GetOrLoadEXIF(const std::wstring& filePath,
EXIFMetadata& outMetadata,
const EXIFLoader& loader)
{
return GetOrLoadInternal<EXIFMetadata, CacheEntry<EXIFMetadata>>(filePath, outMetadata, exifCache, exifMutex, loader);
}
bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath,
XMPMetadata& outMetadata,
const XMPLoader& loader)
{
return GetOrLoadInternal<XMPMetadata, CacheEntry<XMPMetadata>>(filePath, outMetadata, xmpCache, xmpMutex, loader);
}
void MetadataResultCache::ClearAll()
{
{
std::unique_lock lock(exifMutex);
exifCache.clear();
}
{
std::unique_lock lock(xmpMutex);
xmpCache.clear();
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include "MetadataTypes.h"
#include <shared_mutex>
#include <unordered_map>
#include <string>
#include <functional>
namespace PowerRenameLib
{
class MetadataResultCache
{
public:
using EXIFLoader = std::function<bool(EXIFMetadata&)>;
using XMPLoader = std::function<bool(XMPMetadata&)>;
bool GetOrLoadEXIF(const std::wstring& filePath, EXIFMetadata& outMetadata, const EXIFLoader& loader);
bool GetOrLoadXMP(const std::wstring& filePath, XMPMetadata& outMetadata, const XMPLoader& loader);
void ClearAll();
private:
// Wrapper to cache both success and failure states
template<typename T>
struct CacheEntry
{
bool wasSuccessful;
T data;
};
mutable std::shared_mutex exifMutex;
mutable std::shared_mutex xmpMutex;
std::unordered_map<std::wstring, CacheEntry<EXIFMetadata>> exifCache;
std::unordered_map<std::wstring, CacheEntry<XMPMetadata>> xmpCache;
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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