[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

@@ -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;
}