Files
PowerToys/src/modules/powerrename/lib/WICMetadataExtractor.cpp
moooyo 72fc8288eb Add HEIF/AVIF EXIF metadata extraction and UI support (#44466)
- Support EXIF extraction from HEIF/HEIC and AVIF images by detecting
container format and using correct WIC metadata paths.
- Extend supported file extensions to include .heic, .heif, and .avif.
- Add unit tests and test data for HEIF/AVIF extraction, with graceful
handling if required Windows Store extensions are missing.
- Update PowerRename settings UI to show HEIF/AVIF extension status and
provide install buttons.
- Extend ViewModel to detect/install required extensions and expose
status for binding.

<!-- 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

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43758
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **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>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-08 11:18:47 +08:00

1116 lines
40 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 for JPEG format
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
// WIC metadata property paths for TIFF/HEIF format (uses /ifd prefix directly)
// HEIF/HEIC images use TIFF-style metadata paths
const std::wstring HEIF_DATE_TAKEN = L"/ifd/exif/{ushort=36867}"; // DateTimeOriginal
const std::wstring HEIF_DATE_DIGITIZED = L"/ifd/exif/{ushort=36868}"; // DateTimeDigitized
const std::wstring HEIF_DATE_MODIFIED = L"/ifd/{ushort=306}"; // DateTime
const std::wstring HEIF_CAMERA_MAKE = L"/ifd/{ushort=271}"; // Make
const std::wstring HEIF_CAMERA_MODEL = L"/ifd/{ushort=272}"; // Model
const std::wstring HEIF_LENS_MODEL = L"/ifd/exif/{ushort=42036}"; // LensModel
const std::wstring HEIF_ISO = L"/ifd/exif/{ushort=34855}"; // ISOSpeedRatings
const std::wstring HEIF_APERTURE = L"/ifd/exif/{ushort=33437}"; // FNumber
const std::wstring HEIF_SHUTTER_SPEED = L"/ifd/exif/{ushort=33434}"; // ExposureTime
const std::wstring HEIF_FOCAL_LENGTH = L"/ifd/exif/{ushort=37386}"; // FocalLength
const std::wstring HEIF_EXPOSURE_BIAS = L"/ifd/exif/{ushort=37380}"; // ExposureBiasValue
const std::wstring HEIF_FLASH = L"/ifd/exif/{ushort=37385}"; // Flash
const std::wstring HEIF_ORIENTATION = L"/ifd/{ushort=274}"; // Orientation
const std::wstring HEIF_COLOR_SPACE = L"/ifd/exif/{ushort=40961}"; // ColorSpace
const std::wstring HEIF_WIDTH = L"/ifd/exif/{ushort=40962}"; // PixelXDimension
const std::wstring HEIF_HEIGHT = L"/ifd/exif/{ushort=40963}"; // PixelYDimension
const std::wstring HEIF_ARTIST = L"/ifd/{ushort=315}"; // Artist
const std::wstring HEIF_COPYRIGHT = L"/ifd/{ushort=33432}"; // Copyright
// GPS paths for TIFF/HEIF format
const std::wstring HEIF_GPS_LATITUDE = L"/ifd/gps/{ushort=2}"; // GPSLatitude
const std::wstring HEIF_GPS_LATITUDE_REF = L"/ifd/gps/{ushort=1}"; // GPSLatitudeRef
const std::wstring HEIF_GPS_LONGITUDE = L"/ifd/gps/{ushort=4}"; // GPSLongitude
const std::wstring HEIF_GPS_LONGITUDE_REF = L"/ifd/gps/{ushort=3}"; // GPSLongitudeRef
const std::wstring HEIF_GPS_ALTITUDE = L"/ifd/gps/{ushort=6}"; // GPSAltitude
const std::wstring HEIF_GPS_ALTITUDE_REF = L"/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;
}
// Detect container format to determine correct metadata paths
MetadataPathFormat pathFormat = GetMetadataPathFormatFromDecoder(decoder);
ExtractAllEXIFFields(reader, outMetadata, pathFormat);
ExtractGPSData(reader, outMetadata, pathFormat);
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;
}
MetadataPathFormat WICMetadataExtractor::GetMetadataPathFormatFromDecoder(IWICBitmapDecoder* decoder)
{
if (!decoder)
{
return MetadataPathFormat::JPEG;
}
GUID containerFormat;
if (SUCCEEDED(decoder->GetContainerFormat(&containerFormat)))
{
// HEIF and TIFF use /ifd/... paths directly
if (containerFormat == GUID_ContainerFormatHeif ||
containerFormat == GUID_ContainerFormatTiff)
{
return MetadataPathFormat::IFD;
}
}
// JPEG and other formats use /app1/ifd/... paths
return MetadataPathFormat::JPEG;
}
void WICMetadataExtractor::ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat)
{
if (!reader)
return;
// Select the correct paths based on container format
const bool useIfdPaths = (pathFormat == MetadataPathFormat::IFD);
// Date/time paths
const auto& dateTakenPath = useIfdPaths ? HEIF_DATE_TAKEN : EXIF_DATE_TAKEN;
const auto& dateDigitizedPath = useIfdPaths ? HEIF_DATE_DIGITIZED : EXIF_DATE_DIGITIZED;
const auto& dateModifiedPath = useIfdPaths ? HEIF_DATE_MODIFIED : EXIF_DATE_MODIFIED;
// Camera info paths
const auto& cameraMakePath = useIfdPaths ? HEIF_CAMERA_MAKE : EXIF_CAMERA_MAKE;
const auto& cameraModelPath = useIfdPaths ? HEIF_CAMERA_MODEL : EXIF_CAMERA_MODEL;
const auto& lensModelPath = useIfdPaths ? HEIF_LENS_MODEL : EXIF_LENS_MODEL;
// Shooting parameter paths
const auto& isoPath = useIfdPaths ? HEIF_ISO : EXIF_ISO;
const auto& aperturePath = useIfdPaths ? HEIF_APERTURE : EXIF_APERTURE;
const auto& shutterSpeedPath = useIfdPaths ? HEIF_SHUTTER_SPEED : EXIF_SHUTTER_SPEED;
const auto& focalLengthPath = useIfdPaths ? HEIF_FOCAL_LENGTH : EXIF_FOCAL_LENGTH;
const auto& exposureBiasPath = useIfdPaths ? HEIF_EXPOSURE_BIAS : EXIF_EXPOSURE_BIAS;
const auto& flashPath = useIfdPaths ? HEIF_FLASH : EXIF_FLASH;
// Image property paths
const auto& widthPath = useIfdPaths ? HEIF_WIDTH : EXIF_WIDTH;
const auto& heightPath = useIfdPaths ? HEIF_HEIGHT : EXIF_HEIGHT;
const auto& orientationPath = useIfdPaths ? HEIF_ORIENTATION : EXIF_ORIENTATION;
const auto& colorSpacePath = useIfdPaths ? HEIF_COLOR_SPACE : EXIF_COLOR_SPACE;
// Author info paths
const auto& artistPath = useIfdPaths ? HEIF_ARTIST : EXIF_ARTIST;
const auto& copyrightPath = useIfdPaths ? HEIF_COPYRIGHT : EXIF_COPYRIGHT;
// Extract date/time fields
metadata.dateTaken = ReadDateTime(reader, dateTakenPath);
metadata.dateDigitized = ReadDateTime(reader, dateDigitizedPath);
metadata.dateModified = ReadDateTime(reader, dateModifiedPath);
// Extract camera information
metadata.cameraMake = ReadString(reader, cameraMakePath);
metadata.cameraModel = ReadString(reader, cameraModelPath);
metadata.lensModel = ReadString(reader, lensModelPath);
// Extract shooting parameters
metadata.iso = ReadInteger(reader, isoPath);
metadata.aperture = ReadDouble(reader, aperturePath);
metadata.shutterSpeed = ReadDouble(reader, shutterSpeedPath);
metadata.focalLength = ReadDouble(reader, focalLengthPath);
metadata.exposureBias = ReadDouble(reader, exposureBiasPath);
metadata.flash = ReadInteger(reader, flashPath);
// Extract image properties
metadata.width = ReadInteger(reader, widthPath);
metadata.height = ReadInteger(reader, heightPath);
metadata.orientation = ReadInteger(reader, orientationPath);
metadata.colorSpace = ReadInteger(reader, colorSpacePath);
// Extract author information
metadata.author = ReadString(reader, artistPath);
metadata.copyright = ReadString(reader, copyrightPath);
}
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat)
{
if (!reader)
{
return;
}
// Select the correct paths based on container format
const bool useIfdPaths = (pathFormat == MetadataPathFormat::IFD);
const auto& latitudePath = useIfdPaths ? HEIF_GPS_LATITUDE : GPS_LATITUDE;
const auto& longitudePath = useIfdPaths ? HEIF_GPS_LONGITUDE : GPS_LONGITUDE;
const auto& latitudeRefPath = useIfdPaths ? HEIF_GPS_LATITUDE_REF : GPS_LATITUDE_REF;
const auto& longitudeRefPath = useIfdPaths ? HEIF_GPS_LONGITUDE_REF : GPS_LONGITUDE_REF;
const auto& altitudePath = useIfdPaths ? HEIF_GPS_ALTITUDE : GPS_ALTITUDE;
auto lat = ReadMetadata(reader, latitudePath);
auto lon = ReadMetadata(reader, longitudePath);
auto latRef = ReadMetadata(reader, latitudeRefPath);
auto lonRef = ReadMetadata(reader, longitudeRefPath);
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, altitudePath);
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);
}