Files
PowerToys/src/modules/powerrename/lib/WICMetadataExtractor.cpp
moooyo 70e1177a6a [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>
2025-11-04 09:27:16 +08:00

1022 lines
35 KiB
C++

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "pch.h"
#include "WICMetadataExtractor.h"
#include "MetadataFormatHelper.h"
#include <algorithm>
#include <sstream>
#include <iomanip>
#include <cwctype>
#include <comdef.h>
#include <shlwapi.h>
using namespace PowerRenameLib;
namespace
{
// Documentation: https://learn.microsoft.com/en-us/windows/win32/wic/-wic-native-image-format-metadata-queries
// WIC metadata property paths
const std::wstring EXIF_DATE_TAKEN = L"/app1/ifd/exif/{ushort=36867}"; // DateTimeOriginal
const std::wstring EXIF_DATE_DIGITIZED = L"/app1/ifd/exif/{ushort=36868}"; // DateTimeDigitized
const std::wstring EXIF_DATE_MODIFIED = L"/app1/ifd/{ushort=306}"; // DateTime
const std::wstring EXIF_CAMERA_MAKE = L"/app1/ifd/{ushort=271}"; // Make
const std::wstring EXIF_CAMERA_MODEL = L"/app1/ifd/{ushort=272}"; // Model
const std::wstring EXIF_LENS_MODEL = L"/app1/ifd/exif/{ushort=42036}"; // LensModel
const std::wstring EXIF_ISO = L"/app1/ifd/exif/{ushort=34855}"; // ISOSpeedRatings
const std::wstring EXIF_APERTURE = L"/app1/ifd/exif/{ushort=33437}"; // FNumber
const std::wstring EXIF_SHUTTER_SPEED = L"/app1/ifd/exif/{ushort=33434}"; // ExposureTime
const std::wstring EXIF_FOCAL_LENGTH = L"/app1/ifd/exif/{ushort=37386}"; // FocalLength
const std::wstring EXIF_EXPOSURE_BIAS = L"/app1/ifd/exif/{ushort=37380}"; // ExposureBiasValue
const std::wstring EXIF_FLASH = L"/app1/ifd/exif/{ushort=37385}"; // Flash
const std::wstring EXIF_ORIENTATION = L"/app1/ifd/{ushort=274}"; // Orientation
const std::wstring EXIF_COLOR_SPACE = L"/app1/ifd/exif/{ushort=40961}"; // ColorSpace
const std::wstring EXIF_WIDTH = L"/app1/ifd/exif/{ushort=40962}"; // PixelXDimension - actual image width
const std::wstring EXIF_HEIGHT = L"/app1/ifd/exif/{ushort=40963}"; // PixelYDimension - actual image height
const std::wstring EXIF_ARTIST = L"/app1/ifd/{ushort=315}"; // Artist
const std::wstring EXIF_COPYRIGHT = L"/app1/ifd/{ushort=33432}"; // Copyright
// GPS paths
const std::wstring GPS_LATITUDE = L"/app1/ifd/gps/{ushort=2}"; // GPSLatitude
const std::wstring GPS_LATITUDE_REF = L"/app1/ifd/gps/{ushort=1}"; // GPSLatitudeRef
const std::wstring GPS_LONGITUDE = L"/app1/ifd/gps/{ushort=4}"; // GPSLongitude
const std::wstring GPS_LONGITUDE_REF = L"/app1/ifd/gps/{ushort=3}"; // GPSLongitudeRef
const std::wstring GPS_ALTITUDE = L"/app1/ifd/gps/{ushort=6}"; // GPSAltitude
const std::wstring GPS_ALTITUDE_REF = L"/app1/ifd/gps/{ushort=5}"; // GPSAltitudeRef
// Documentation: https://developer.adobe.com/xmp/docs/XMPNamespaces/xmp/
// Based on actual WIC path format discovered through enumeration
// XMP Basic schema - xmp: namespace
const std::wstring XMP_CREATE_DATE = L"/xmp/xmp:CreateDate"; // XMP Create Date
const std::wstring XMP_MODIFY_DATE = L"/xmp/xmp:ModifyDate"; // XMP Modify Date
const std::wstring XMP_METADATA_DATE = L"/xmp/xmp:MetadataDate"; // XMP Metadata Date
const std::wstring XMP_CREATOR_TOOL = L"/xmp/xmp:CreatorTool"; // XMP Creator Tool
// Dublin Core schema - dc: namespace
// Note: For language alternatives like title/description, we need to append /x-default
const std::wstring XMP_DC_TITLE = L"/xmp/dc:title/x-default"; // Title (default language)
const std::wstring XMP_DC_DESCRIPTION = L"/xmp/dc:description/x-default"; // Description (default language)
const std::wstring XMP_DC_CREATOR = L"/xmp/dc:creator"; // Creator/Author
const std::wstring XMP_DC_SUBJECT = L"/xmp/dc:subject"; // Subject/Keywords (array)
// XMP Rights Management schema - xmpRights: namespace
const std::wstring XMP_RIGHTS = L"/xmp/xmpRights:WebStatement"; // Copyright/Rights
// XMP Media Management schema - xmpMM: namespace
const std::wstring XMP_MM_DOCUMENT_ID = L"/xmp/xmpMM:DocumentID"; // Document ID
const std::wstring XMP_MM_INSTANCE_ID = L"/xmp/xmpMM:InstanceID"; // Instance ID
const std::wstring XMP_MM_ORIGINAL_DOCUMENT_ID = L"/xmp/xmpMM:OriginalDocumentID"; // Original Document ID
const std::wstring XMP_MM_VERSION_ID = L"/xmp/xmpMM:VersionID"; // Version ID
std::wstring TrimWhitespace(const std::wstring& value)
{
const auto first = value.find_first_not_of(L" \t\r\n");
if (first == std::wstring::npos)
{
return {};
}
const auto last = value.find_last_not_of(L" \t\r\n");
return value.substr(first, last - first + 1);
}
bool TryParseFixedWidthInt(const std::wstring& source, size_t start, size_t length, int& value)
{
if (start + length > source.size())
{
return false;
}
int result = 0;
for (size_t i = 0; i < length; ++i)
{
const wchar_t ch = source[start + i];
if (ch < L'0' || ch > L'9')
{
return false;
}
result = result * 10 + static_cast<int>(ch - L'0');
}
value = result;
return true;
}
bool ValidateAndBuildSystemTime(int year, int month, int day, int hour, int minute, int second, int milliseconds, SYSTEMTIME& outTime)
{
if (year < 1601 || year > 9999 ||
month < 1 || month > 12 ||
day < 1 || day > 31 ||
hour < 0 || hour > 23 ||
minute < 0 || minute > 59 ||
second < 0 || second > 59 ||
milliseconds < 0 || milliseconds > 999)
{
return false;
}
SYSTEMTIME candidate{};
candidate.wYear = static_cast<WORD>(year);
candidate.wMonth = static_cast<WORD>(month);
candidate.wDay = static_cast<WORD>(day);
candidate.wHour = static_cast<WORD>(hour);
candidate.wMinute = static_cast<WORD>(minute);
candidate.wSecond = static_cast<WORD>(second);
candidate.wMilliseconds = static_cast<WORD>(milliseconds);
FILETIME fileTime{};
if (!SystemTimeToFileTime(&candidate, &fileTime))
{
return false;
}
outTime = candidate;
return true;
}
std::optional<SYSTEMTIME> ParseExifDateTime(const std::wstring& date)
{
if (date.size() < 19)
{
return std::nullopt;
}
if (date[4] != L':' || date[7] != L':' ||
(date[10] != L' ' && date[10] != L'T') ||
date[13] != L':' || date[16] != L':')
{
return std::nullopt;
}
int year = 0;
int month = 0;
int day = 0;
int hour = 0;
int minute = 0;
int second = 0;
if (!TryParseFixedWidthInt(date, 0, 4, year) ||
!TryParseFixedWidthInt(date, 5, 2, month) ||
!TryParseFixedWidthInt(date, 8, 2, day) ||
!TryParseFixedWidthInt(date, 11, 2, hour) ||
!TryParseFixedWidthInt(date, 14, 2, minute) ||
!TryParseFixedWidthInt(date, 17, 2, second))
{
return std::nullopt;
}
int milliseconds = 0;
size_t pos = 19;
if (pos < date.size() && (date[pos] == L'.' || date[pos] == L','))
{
++pos;
int digits = 0;
while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3)
{
milliseconds = milliseconds * 10 + static_cast<int>(date[pos] - L'0');
++pos;
++digits;
}
while (digits > 0 && digits < 3)
{
milliseconds *= 10;
++digits;
}
}
SYSTEMTIME result{};
if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, result))
{
return std::nullopt;
}
return result;
}
std::optional<SYSTEMTIME> ParseIso8601DateTime(const std::wstring& date)
{
if (date.size() < 19)
{
return std::nullopt;
}
size_t separator = date.find(L'T');
if (separator == std::wstring::npos)
{
separator = date.find(L' ');
}
if (separator == std::wstring::npos)
{
return std::nullopt;
}
int year = 0;
int month = 0;
int day = 0;
if (!TryParseFixedWidthInt(date, 0, 4, year) ||
date[4] != L'-' ||
!TryParseFixedWidthInt(date, 5, 2, month) ||
date[7] != L'-' ||
!TryParseFixedWidthInt(date, 8, 2, day))
{
return std::nullopt;
}
size_t timePos = separator + 1;
if (timePos + 7 >= date.size())
{
return std::nullopt;
}
int hour = 0;
int minute = 0;
int second = 0;
if (!TryParseFixedWidthInt(date, timePos, 2, hour) ||
date[timePos + 2] != L':' ||
!TryParseFixedWidthInt(date, timePos + 3, 2, minute) ||
date[timePos + 5] != L':' ||
!TryParseFixedWidthInt(date, timePos + 6, 2, second))
{
return std::nullopt;
}
size_t pos = timePos + 8;
int milliseconds = 0;
if (pos < date.size() && (date[pos] == L'.' || date[pos] == L','))
{
++pos;
int digits = 0;
while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3)
{
milliseconds = milliseconds * 10 + static_cast<int>(date[pos] - L'0');
++pos;
++digits;
}
while (pos < date.size() && std::iswdigit(date[pos]))
{
++pos;
}
while (digits > 0 && digits < 3)
{
milliseconds *= 10;
++digits;
}
}
bool hasOffset = false;
int offsetMinutes = 0;
if (pos < date.size())
{
const wchar_t tzIndicator = date[pos];
if (tzIndicator == L'Z' || tzIndicator == L'z')
{
hasOffset = true;
offsetMinutes = 0;
++pos;
}
else if (tzIndicator == L'+' || tzIndicator == L'-')
{
hasOffset = true;
const int sign = (tzIndicator == L'-') ? -1 : 1;
++pos;
int offsetHours = 0;
int offsetMins = 0;
if (!TryParseFixedWidthInt(date, pos, 2, offsetHours))
{
return std::nullopt;
}
pos += 2;
if (pos < date.size() && date[pos] == L':')
{
++pos;
}
if (pos + 1 < date.size() && std::iswdigit(date[pos]) && std::iswdigit(date[pos + 1]))
{
if (!TryParseFixedWidthInt(date, pos, 2, offsetMins))
{
return std::nullopt;
}
pos += 2;
}
if (offsetHours < 0 || offsetHours > 23 || offsetMins < 0 || offsetMins > 59)
{
return std::nullopt;
}
offsetMinutes = sign * (offsetHours * 60 + offsetMins);
}
while (pos < date.size() && std::iswspace(date[pos]))
{
++pos;
}
if (pos != date.size())
{
return std::nullopt;
}
}
SYSTEMTIME baseTime{};
if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, baseTime))
{
return std::nullopt;
}
if (!hasOffset)
{
return baseTime;
}
FILETIME utcFileTime{};
if (!SystemTimeToFileTime(&baseTime, &utcFileTime))
{
return std::nullopt;
}
ULARGE_INTEGER timeValue{};
timeValue.LowPart = utcFileTime.dwLowDateTime;
timeValue.HighPart = utcFileTime.dwHighDateTime;
constexpr long long TicksPerMinute = 60LL * 10000000LL;
timeValue.QuadPart -= static_cast<long long>(offsetMinutes) * TicksPerMinute;
FILETIME adjustedUtc{};
adjustedUtc.dwLowDateTime = timeValue.LowPart;
adjustedUtc.dwHighDateTime = timeValue.HighPart;
FILETIME localFileTime{};
if (!FileTimeToLocalFileTime(&adjustedUtc, &localFileTime))
{
return std::nullopt;
}
SYSTEMTIME localTime{};
if (!FileTimeToSystemTime(&localFileTime, &localTime))
{
return std::nullopt;
}
return localTime;
}
// Global WIC factory management with thread-safe access
CComPtr<IWICImagingFactory> g_wicFactory;
std::once_flag g_wicInitFlag;
std::mutex g_wicFactoryMutex; // Protect access to g_wicFactory
}
WICMetadataExtractor::WICMetadataExtractor()
{
InitializeWIC();
}
WICMetadataExtractor::~WICMetadataExtractor()
{
// WIC cleanup handled statically
}
void WICMetadataExtractor::InitializeWIC()
{
std::call_once(g_wicInitFlag, []() {
// Don't initialize COM in library code - assume caller has done it
// Just create the WIC factory
HRESULT hr = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
reinterpret_cast<LPVOID*>(&g_wicFactory)
);
if (FAILED(hr))
{
g_wicFactory = nullptr;
}
});
}
CComPtr<IWICImagingFactory> WICMetadataExtractor::GetWICFactory()
{
std::lock_guard<std::mutex> lock(g_wicFactoryMutex);
return g_wicFactory;
}
bool WICMetadataExtractor::ExtractEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata)
{
return cache.GetOrLoadEXIF(filePath, outMetadata, [this, &filePath](EXIFMetadata& metadata) {
return LoadEXIFMetadata(filePath, metadata);
});
}
bool WICMetadataExtractor::LoadEXIFMetadata(
const std::wstring& filePath,
EXIFMetadata& outMetadata)
{
CComPtr<IWICMetadataQueryReader> reader;
if (!PathFileExistsW(filePath.c_str()))
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: File not found - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
auto decoder = CreateDecoder(filePath);
if (!decoder)
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] EXIF metadata extraction: Unsupported format or unable to create decoder - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: WIC decoder error - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
reader = GetMetadataReader(decoder);
if (!reader)
{
// No metadata is not necessarily an error - just means the file has no EXIF data
return false;
}
ExtractAllEXIFFields(reader, outMetadata);
ExtractGPSData(reader, outMetadata);
return true;
}
void WICMetadataExtractor::ClearCache()
{
cache.ClearAll();
}
CComPtr<IWICBitmapDecoder> WICMetadataExtractor::CreateDecoder(const std::wstring& filePath)
{
auto factory = GetWICFactory();
if (!factory)
{
return nullptr;
}
CComPtr<IWICBitmapDecoder> decoder;
HRESULT hr = factory->CreateDecoderFromFilename(
filePath.c_str(),
nullptr,
GENERIC_READ,
WICDecodeMetadataCacheOnLoad,
&decoder
);
if (FAILED(hr))
{
return nullptr;
}
return decoder;
}
CComPtr<IWICMetadataQueryReader> WICMetadataExtractor::GetMetadataReader(IWICBitmapDecoder* decoder)
{
if (!decoder)
{
return nullptr;
}
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
return nullptr;
}
CComPtr<IWICMetadataQueryReader> reader;
frame->GetMetadataQueryReader(&reader);
return reader;
}
void WICMetadataExtractor::ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
{
if (!reader)
return;
// Extract date/time fields
metadata.dateTaken = ReadDateTime(reader, EXIF_DATE_TAKEN);
metadata.dateDigitized = ReadDateTime(reader, EXIF_DATE_DIGITIZED);
metadata.dateModified = ReadDateTime(reader, EXIF_DATE_MODIFIED);
// Extract camera information
metadata.cameraMake = ReadString(reader, EXIF_CAMERA_MAKE);
metadata.cameraModel = ReadString(reader, EXIF_CAMERA_MODEL);
metadata.lensModel = ReadString(reader, EXIF_LENS_MODEL);
// Extract shooting parameters
metadata.iso = ReadInteger(reader, EXIF_ISO);
metadata.aperture = ReadDouble(reader, EXIF_APERTURE);
metadata.shutterSpeed = ReadDouble(reader, EXIF_SHUTTER_SPEED);
metadata.focalLength = ReadDouble(reader, EXIF_FOCAL_LENGTH);
metadata.exposureBias = ReadDouble(reader, EXIF_EXPOSURE_BIAS);
metadata.flash = ReadInteger(reader, EXIF_FLASH);
// Extract image properties
metadata.width = ReadInteger(reader, EXIF_WIDTH);
metadata.height = ReadInteger(reader, EXIF_HEIGHT);
metadata.orientation = ReadInteger(reader, EXIF_ORIENTATION);
metadata.colorSpace = ReadInteger(reader, EXIF_COLOR_SPACE);
// Extract author information
metadata.author = ReadString(reader, EXIF_ARTIST);
metadata.copyright = ReadString(reader, EXIF_COPYRIGHT);
}
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
{
if (!reader)
{
return;
}
auto lat = ReadMetadata(reader, GPS_LATITUDE);
auto lon = ReadMetadata(reader, GPS_LONGITUDE);
auto latRef = ReadMetadata(reader, GPS_LATITUDE_REF);
auto lonRef = ReadMetadata(reader, GPS_LONGITUDE_REF);
if (lat && lon)
{
PropVariantValue emptyLatRef;
PropVariantValue emptyLonRef;
const PROPVARIANT& latRefVar = latRef ? latRef->Get() : emptyLatRef.Get();
const PROPVARIANT& lonRefVar = lonRef ? lonRef->Get() : emptyLonRef.Get();
auto coords = MetadataFormatHelper::ParseGPSCoordinates(
lat->Get(),
lon->Get(),
latRefVar,
lonRefVar);
metadata.latitude = coords.first;
metadata.longitude = coords.second;
}
auto alt = ReadMetadata(reader, GPS_ALTITUDE);
if (alt)
{
metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get());
}
}
std::optional<SYSTEMTIME> WICMetadataExtractor::ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar)
{
return std::nullopt;
}
std::wstring rawValue;
const PROPVARIANT& variant = propVar->Get();
switch (variant.vt)
{
case VT_LPWSTR:
if (variant.pwszVal)
{
rawValue = variant.pwszVal;
}
break;
case VT_BSTR:
if (variant.bstrVal)
{
rawValue = variant.bstrVal;
}
break;
case VT_LPSTR:
if (variant.pszVal)
{
const int size = MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, nullptr, 0);
if (size > 1)
{
rawValue.resize(static_cast<size_t>(size) - 1);
MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, &rawValue[0], size);
}
}
break;
default:
break;
}
if (rawValue.empty())
{
return std::nullopt;
}
const std::wstring normalized = TrimWhitespace(rawValue);
if (normalized.empty())
{
return std::nullopt;
}
if (auto exifDate = ParseExifDateTime(normalized))
{
return exifDate;
}
if (auto isoDate = ParseIso8601DateTime(normalized))
{
return isoDate;
}
return std::nullopt;
}
std::optional<std::wstring> WICMetadataExtractor::ReadString(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar.has_value())
return std::nullopt;
std::wstring result;
switch (propVar->Get().vt)
{
case VT_LPWSTR:
if (propVar->Get().pwszVal)
result = propVar->Get().pwszVal;
break;
case VT_BSTR:
if (propVar->Get().bstrVal)
result = propVar->Get().bstrVal;
break;
case VT_LPSTR:
if (propVar->Get().pszVal)
{
int size = MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, nullptr, 0);
if (size > 1)
{
result.resize(static_cast<size_t>(size) - 1);
MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, &result[0], size);
}
}
break;
}
// Trim whitespace from both ends
if (!result.empty())
{
size_t start = result.find_first_not_of(L" \t\r\n");
size_t end = result.find_last_not_of(L" \t\r\n");
if (start != std::wstring::npos && end != std::wstring::npos)
{
result = result.substr(start, end - start + 1);
}
else if (start == std::wstring::npos)
{
result.clear();
}
}
return result.empty() ? std::nullopt : std::make_optional(result);
}
std::optional<int64_t> WICMetadataExtractor::ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar.has_value())
return std::nullopt;
int64_t result = 0;
switch (propVar->Get().vt)
{
case VT_I1: result = propVar->Get().cVal; break;
case VT_I2: result = propVar->Get().iVal; break;
case VT_I4: result = propVar->Get().lVal; break;
case VT_I8: result = propVar->Get().hVal.QuadPart; break;
case VT_UI1: result = propVar->Get().bVal; break;
case VT_UI2: result = propVar->Get().uiVal; break;
case VT_UI4: result = propVar->Get().ulVal; break;
case VT_UI8: result = static_cast<int64_t>(propVar->Get().uhVal.QuadPart); break;
default:
return std::nullopt;
}
return result;
}
std::optional<double> WICMetadataExtractor::ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path)
{
auto propVar = ReadMetadata(reader, path);
if (!propVar.has_value())
return std::nullopt;
double result = 0.0;
switch (propVar->Get().vt)
{
case VT_R4:
result = static_cast<double>(propVar->Get().fltVal);
break;
case VT_R8:
result = propVar->Get().dblVal;
break;
case VT_UI1 | VT_VECTOR:
case VT_UI4 | VT_VECTOR:
// Handle rational number (common for EXIF values)
// Rational data is stored as 8 bytes: 4-byte numerator + 4-byte denominator
if (propVar->Get().caub.cElems >= 8)
{
// ExposureBias (EXIF tag 37380) uses SRATIONAL type (signed rational)
// which can represent negative values like -0.33 EV for exposure compensation.
// Most other EXIF fields use RATIONAL type (unsigned) for values like aperture, shutter speed.
if (path == EXIF_EXPOSURE_BIAS)
{
// Parse as signed rational: int32_t / int32_t
result = MetadataFormatHelper::ParseSingleSRational(propVar->Get().caub.pElems, 0);
break;
}
else
{
// Parse as unsigned rational: uint32_t / uint32_t
// First check if denominator is valid (non-zero) to avoid division by zero
const uint8_t* bytes = propVar->Get().caub.pElems;
uint32_t denominator = static_cast<uint32_t>(bytes[4]) |
(static_cast<uint32_t>(bytes[5]) << 8) |
(static_cast<uint32_t>(bytes[6]) << 16) |
(static_cast<uint32_t>(bytes[7]) << 24);
if (denominator != 0)
{
result = MetadataFormatHelper::ParseSingleRational(propVar->Get().caub.pElems, 0);
break;
}
}
}
return std::nullopt;
default:
// Try integer conversion
switch (propVar->Get().vt)
{
case VT_I1: result = static_cast<double>(propVar->Get().cVal); break;
case VT_I2: result = static_cast<double>(propVar->Get().iVal); break;
case VT_I4: result = static_cast<double>(propVar->Get().lVal); break;
case VT_I8:
{
// ExposureBias (EXIF tag 37380) may be stored as VT_I8 in some WIC implementations
// It represents a signed rational (SRATIONAL) packed into a 64-bit integer
if (path == EXIF_EXPOSURE_BIAS)
{
// Parse signed rational from int64: low 32 bits = numerator, high 32 bits = denominator
// Some implementations may reverse the order, so we try both
int32_t numerator = static_cast<int32_t>(propVar->Get().hVal.QuadPart & 0xFFFFFFFF);
int32_t denominator = static_cast<int32_t>(propVar->Get().hVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Try reversed order: high 32 bits = numerator, low 32 bits = denominator
numerator = static_cast<int32_t>(propVar->Get().hVal.QuadPart >> 32);
denominator = static_cast<int32_t>(propVar->Get().hVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
result = 0.0; // Default to 0 if both attempts fail
}
}
}
else
{
// For other fields, treat VT_I8 as a simple 64-bit integer
result = static_cast<double>(propVar->Get().hVal.QuadPart);
}
}
break;
case VT_UI1: result = static_cast<double>(propVar->Get().bVal); break;
case VT_UI2: result = static_cast<double>(propVar->Get().uiVal); break;
case VT_UI4: result = static_cast<double>(propVar->Get().ulVal); break;
case VT_UI8:
{
// ExposureBias (EXIF tag 37380) may be stored as VT_UI8 in some WIC implementations
// Even though it's unsigned, we need to reinterpret it as signed for SRATIONAL
if (path == EXIF_EXPOSURE_BIAS)
{
// Parse signed rational from uint64 (reinterpret as signed)
// Low 32 bits = numerator, high 32 bits = denominator
int32_t numerator = static_cast<int32_t>(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
int32_t denominator = static_cast<int32_t>(propVar->Get().uhVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Try reversed order: high 32 bits = numerator, low 32 bits = denominator
numerator = static_cast<int32_t>(propVar->Get().uhVal.QuadPart >> 32);
denominator = static_cast<int32_t>(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
result = 0.0; // Default to 0 if both attempts fail
}
}
}
else
{
// For other EXIF rational fields (unsigned), try both byte orders to handle different encodings
// First try: low 32 bits = numerator, high 32 bits = denominator
uint32_t numerator = static_cast<uint32_t>(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
uint32_t denominator = static_cast<uint32_t>(propVar->Get().uhVal.QuadPart >> 32);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Second try: high 32 bits = numerator, low 32 bits = denominator
numerator = static_cast<uint32_t>(propVar->Get().uhVal.QuadPart >> 32);
denominator = static_cast<uint32_t>(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF);
if (denominator != 0)
{
result = static_cast<double>(numerator) / static_cast<double>(denominator);
}
else
{
// Fall back to treating as regular integer if denominator is 0
result = static_cast<double>(propVar->Get().uhVal.QuadPart);
}
}
}
}
break;
default:
return std::nullopt;
}
}
return result;
}
std::optional<PropVariantValue> WICMetadataExtractor::ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path)
{
if (!reader)
return std::nullopt;
PropVariantValue value;
HRESULT hr = reader->GetMetadataByName(path.c_str(), value.GetAddressOf());
if (SUCCEEDED(hr))
{
return std::optional<PropVariantValue>(std::move(value));
}
return std::nullopt;
}
// GPS parsing functions have been moved to MetadataFormatHelper for better testability
bool WICMetadataExtractor::ExtractXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata)
{
return cache.GetOrLoadXMP(filePath, outMetadata, [this, &filePath](XMPMetadata& metadata) {
return LoadXMPMetadata(filePath, metadata);
});
}
bool WICMetadataExtractor::LoadXMPMetadata(
const std::wstring& filePath,
XMPMetadata& outMetadata)
{
if (!PathFileExistsW(filePath.c_str()))
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] XMP metadata extraction failed: File not found - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
auto decoder = CreateDecoder(filePath);
if (!decoder)
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] XMP metadata extraction: Unsupported format or unable to create decoder - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
CComPtr<IWICBitmapFrameDecode> frame;
if (FAILED(decoder->GetFrame(0, &frame)))
{
#ifdef _DEBUG
std::wstring msg = L"[PowerRename] XMP metadata extraction failed: WIC decoder error - " + filePath + L"\n";
OutputDebugStringW(msg.c_str());
#endif
return false;
}
CComPtr<IWICMetadataQueryReader> rootReader;
if (FAILED(frame->GetMetadataQueryReader(&rootReader)))
{
// No metadata is not necessarily an error - just means the file has no XMP data
return false;
}
ExtractAllXMPFields(rootReader, outMetadata);
return true;
}
// Batch extraction method implementations
void WICMetadataExtractor::ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata)
{
if (!reader)
return;
// XMP Basic schema - xmp: namespace
metadata.creatorTool = ReadString(reader, XMP_CREATOR_TOOL);
metadata.createDate = ReadDateTime(reader, XMP_CREATE_DATE);
metadata.modifyDate = ReadDateTime(reader, XMP_MODIFY_DATE);
metadata.metadataDate = ReadDateTime(reader, XMP_METADATA_DATE);
// Dublin Core schema - dc: namespace
metadata.title = ReadString(reader, XMP_DC_TITLE);
metadata.description = ReadString(reader, XMP_DC_DESCRIPTION);
metadata.creator = ReadString(reader, XMP_DC_CREATOR);
// For dc:subject, we need to handle the array structure
// Try to read individual elements
// XMP allows for large arrays, but we limit to a reasonable number to avoid performance issues
constexpr int MAX_XMP_SUBJECTS = 50;
std::vector<std::wstring> subjects;
for (int i = 0; i < MAX_XMP_SUBJECTS; ++i)
{
std::wstring subjectPath = L"/xmp/dc:subject/{ulong=" + std::to_wstring(i) + L"}";
auto subject = ReadString(reader, subjectPath);
if (subject.has_value())
{
subjects.push_back(subject.value());
}
else
{
break; // No more subjects
}
}
if (!subjects.empty())
{
metadata.subject = subjects;
}
// XMP Rights Management schema
metadata.rights = ReadString(reader, XMP_RIGHTS);
// XMP Media Management schema - xmpMM: namespace
metadata.documentID = ReadString(reader, XMP_MM_DOCUMENT_ID);
metadata.instanceID = ReadString(reader, XMP_MM_INSTANCE_ID);
metadata.originalDocumentID = ReadString(reader, XMP_MM_ORIGINAL_DOCUMENT_ID);
metadata.versionID = ReadString(reader, XMP_MM_VERSION_ID);
}