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>
This commit is contained in:
moooyo
2026-01-08 11:18:47 +08:00
committed by GitHub
parent 9c58574484
commit 72fc8288eb
12 changed files with 724 additions and 46 deletions

View File

@@ -347,14 +347,19 @@ bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataTyp
// 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
// - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
// - PNG (text chunks)
// - HEIF/HEIC (IFD, Exif, XMP, GPS) - requires HEIF Image Extensions from Microsoft Store
// - AVIF (IFD, Exif, XMP, GPS) - requires AV1 Video Extension from Microsoft Store
static const std::unordered_set<std::wstring> supportedExtensions = {
L".jpg",
L".jpeg",
L".png",
L".tif",
L".tiff"
L".tiff",
L".heic",
L".heif",
L".avif"
};
// If file type doesn't support metadata, no need to check patterns

View File

@@ -20,7 +20,7 @@ namespace
// 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_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
@@ -37,14 +37,43 @@ namespace
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
// 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/
@@ -465,8 +494,11 @@ bool WICMetadataExtractor::LoadEXIFMetadata(
return false;
}
ExtractAllEXIFFields(reader, outMetadata);
ExtractGPSData(reader, outMetadata);
// Detect container format to determine correct metadata paths
MetadataPathFormat pathFormat = GetMetadataPathFormatFromDecoder(decoder);
ExtractAllEXIFFields(reader, outMetadata, pathFormat);
ExtractGPSData(reader, outMetadata, pathFormat);
return true;
}
@@ -507,64 +539,126 @@ CComPtr<IWICMetadataQueryReader> WICMetadataExtractor::GetMetadataReader(IWICBit
{
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)
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, EXIF_DATE_TAKEN);
metadata.dateDigitized = ReadDateTime(reader, EXIF_DATE_DIGITIZED);
metadata.dateModified = ReadDateTime(reader, EXIF_DATE_MODIFIED);
metadata.dateTaken = ReadDateTime(reader, dateTakenPath);
metadata.dateDigitized = ReadDateTime(reader, dateDigitizedPath);
metadata.dateModified = ReadDateTime(reader, dateModifiedPath);
// Extract camera information
metadata.cameraMake = ReadString(reader, EXIF_CAMERA_MAKE);
metadata.cameraModel = ReadString(reader, EXIF_CAMERA_MODEL);
metadata.lensModel = ReadString(reader, EXIF_LENS_MODEL);
metadata.cameraMake = ReadString(reader, cameraMakePath);
metadata.cameraModel = ReadString(reader, cameraModelPath);
metadata.lensModel = ReadString(reader, lensModelPath);
// 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);
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, EXIF_WIDTH);
metadata.height = ReadInteger(reader, EXIF_HEIGHT);
metadata.orientation = ReadInteger(reader, EXIF_ORIENTATION);
metadata.colorSpace = ReadInteger(reader, EXIF_COLOR_SPACE);
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, EXIF_ARTIST);
metadata.copyright = ReadString(reader, EXIF_COPYRIGHT);
metadata.author = ReadString(reader, artistPath);
metadata.copyright = ReadString(reader, copyrightPath);
}
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat)
{
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);
// 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)
{
@@ -584,7 +678,7 @@ void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFM
metadata.longitude = coords.second;
}
auto alt = ReadMetadata(reader, GPS_ALTITUDE);
auto alt = ReadMetadata(reader, altitudePath);
if (alt)
{
metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get());

View File

@@ -9,14 +9,32 @@
#include <wincodec.h>
#include <atlbase.h>
// Forward declarations for unit test friend classes
namespace WICMetadataExtractorTests
{
class ExtractAVIFMetadataTests;
}
namespace PowerRenameLib
{
/// <summary>
/// Metadata path format based on container type
/// </summary>
enum class MetadataPathFormat
{
JPEG, // Uses /app1/ifd/... paths (JPEG)
IFD // Uses /ifd/... paths (HEIF, TIFF, etc.)
};
/// <summary>
/// Windows Imaging Component (WIC) implementation for metadata extraction
/// Provides efficient batch extraction of all metadata types with built-in caching
/// </summary>
class WICMetadataExtractor
{
// Friend declarations for unit testing
friend class WICMetadataExtractorTests::ExtractAVIFMetadataTests;
public:
WICMetadataExtractor();
~WICMetadataExtractor();
@@ -45,10 +63,13 @@ namespace PowerRenameLib
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
// Batch extraction methods
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat);
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat);
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
// Internal container format detection
MetadataPathFormat GetMetadataPathFormatFromDecoder(IWICBitmapDecoder* decoder);
// Field reading helpers
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);