mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-07 11:46:30 +02:00
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:
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -273,3 +273,6 @@ St&yle
|
|||||||
# Usernames with numbers
|
# Usernames with numbers
|
||||||
# 0x6f677548 is user name but user folder causes a flag
|
# 0x6f677548 is user name but user folder causes a flag
|
||||||
\bx6f677548\b
|
\bx6f677548\b
|
||||||
|
|
||||||
|
# Microsoft Store URLs and product IDs
|
||||||
|
ms-windows-store://\S+
|
||||||
|
|||||||
@@ -347,14 +347,19 @@ bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataTyp
|
|||||||
|
|
||||||
// According to the metadata support table, only these formats support metadata extraction:
|
// According to the metadata support table, only these formats support metadata extraction:
|
||||||
// - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding
|
// - 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)
|
// - 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 = {
|
static const std::unordered_set<std::wstring> supportedExtensions = {
|
||||||
L".jpg",
|
L".jpg",
|
||||||
L".jpeg",
|
L".jpeg",
|
||||||
L".png",
|
L".png",
|
||||||
L".tif",
|
L".tif",
|
||||||
L".tiff"
|
L".tiff",
|
||||||
|
L".heic",
|
||||||
|
L".heif",
|
||||||
|
L".avif"
|
||||||
};
|
};
|
||||||
|
|
||||||
// If file type doesn't support metadata, no need to check patterns
|
// If file type doesn't support metadata, no need to check patterns
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace
|
|||||||
|
|
||||||
// WIC metadata property paths
|
// WIC metadata property paths
|
||||||
const std::wstring EXIF_DATE_TAKEN = L"/app1/ifd/exif/{ushort=36867}"; // DateTimeOriginal
|
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_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_MAKE = L"/app1/ifd/{ushort=271}"; // Make
|
||||||
const std::wstring EXIF_CAMERA_MODEL = L"/app1/ifd/{ushort=272}"; // Model
|
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_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_ARTIST = L"/app1/ifd/{ushort=315}"; // Artist
|
||||||
const std::wstring EXIF_COPYRIGHT = L"/app1/ifd/{ushort=33432}"; // Copyright
|
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 = L"/app1/ifd/gps/{ushort=2}"; // GPSLatitude
|
||||||
const std::wstring GPS_LATITUDE_REF = L"/app1/ifd/gps/{ushort=1}"; // GPSLatitudeRef
|
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 = L"/app1/ifd/gps/{ushort=4}"; // GPSLongitude
|
||||||
const std::wstring GPS_LONGITUDE_REF = L"/app1/ifd/gps/{ushort=3}"; // GPSLongitudeRef
|
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 = L"/app1/ifd/gps/{ushort=6}"; // GPSAltitude
|
||||||
const std::wstring GPS_ALTITUDE_REF = L"/app1/ifd/gps/{ushort=5}"; // GPSAltitudeRef
|
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/
|
// Documentation: https://developer.adobe.com/xmp/docs/XMPNamespaces/xmp/
|
||||||
@@ -465,8 +494,11 @@ bool WICMetadataExtractor::LoadEXIFMetadata(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtractAllEXIFFields(reader, outMetadata);
|
// Detect container format to determine correct metadata paths
|
||||||
ExtractGPSData(reader, outMetadata);
|
MetadataPathFormat pathFormat = GetMetadataPathFormatFromDecoder(decoder);
|
||||||
|
|
||||||
|
ExtractAllEXIFFields(reader, outMetadata, pathFormat);
|
||||||
|
ExtractGPSData(reader, outMetadata, pathFormat);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -507,64 +539,126 @@ CComPtr<IWICMetadataQueryReader> WICMetadataExtractor::GetMetadataReader(IWICBit
|
|||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
CComPtr<IWICBitmapFrameDecode> frame;
|
CComPtr<IWICBitmapFrameDecode> frame;
|
||||||
if (FAILED(decoder->GetFrame(0, &frame)))
|
if (FAILED(decoder->GetFrame(0, &frame)))
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
CComPtr<IWICMetadataQueryReader> reader;
|
CComPtr<IWICMetadataQueryReader> reader;
|
||||||
frame->GetMetadataQueryReader(&reader);
|
frame->GetMetadataQueryReader(&reader);
|
||||||
|
|
||||||
return 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)
|
if (!reader)
|
||||||
return;
|
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
|
// Extract date/time fields
|
||||||
metadata.dateTaken = ReadDateTime(reader, EXIF_DATE_TAKEN);
|
metadata.dateTaken = ReadDateTime(reader, dateTakenPath);
|
||||||
metadata.dateDigitized = ReadDateTime(reader, EXIF_DATE_DIGITIZED);
|
metadata.dateDigitized = ReadDateTime(reader, dateDigitizedPath);
|
||||||
metadata.dateModified = ReadDateTime(reader, EXIF_DATE_MODIFIED);
|
metadata.dateModified = ReadDateTime(reader, dateModifiedPath);
|
||||||
|
|
||||||
// Extract camera information
|
// Extract camera information
|
||||||
metadata.cameraMake = ReadString(reader, EXIF_CAMERA_MAKE);
|
metadata.cameraMake = ReadString(reader, cameraMakePath);
|
||||||
metadata.cameraModel = ReadString(reader, EXIF_CAMERA_MODEL);
|
metadata.cameraModel = ReadString(reader, cameraModelPath);
|
||||||
metadata.lensModel = ReadString(reader, EXIF_LENS_MODEL);
|
metadata.lensModel = ReadString(reader, lensModelPath);
|
||||||
|
|
||||||
// Extract shooting parameters
|
// Extract shooting parameters
|
||||||
metadata.iso = ReadInteger(reader, EXIF_ISO);
|
metadata.iso = ReadInteger(reader, isoPath);
|
||||||
metadata.aperture = ReadDouble(reader, EXIF_APERTURE);
|
metadata.aperture = ReadDouble(reader, aperturePath);
|
||||||
metadata.shutterSpeed = ReadDouble(reader, EXIF_SHUTTER_SPEED);
|
metadata.shutterSpeed = ReadDouble(reader, shutterSpeedPath);
|
||||||
metadata.focalLength = ReadDouble(reader, EXIF_FOCAL_LENGTH);
|
metadata.focalLength = ReadDouble(reader, focalLengthPath);
|
||||||
metadata.exposureBias = ReadDouble(reader, EXIF_EXPOSURE_BIAS);
|
metadata.exposureBias = ReadDouble(reader, exposureBiasPath);
|
||||||
metadata.flash = ReadInteger(reader, EXIF_FLASH);
|
metadata.flash = ReadInteger(reader, flashPath);
|
||||||
|
|
||||||
// Extract image properties
|
// Extract image properties
|
||||||
metadata.width = ReadInteger(reader, EXIF_WIDTH);
|
metadata.width = ReadInteger(reader, widthPath);
|
||||||
metadata.height = ReadInteger(reader, EXIF_HEIGHT);
|
metadata.height = ReadInteger(reader, heightPath);
|
||||||
metadata.orientation = ReadInteger(reader, EXIF_ORIENTATION);
|
metadata.orientation = ReadInteger(reader, orientationPath);
|
||||||
metadata.colorSpace = ReadInteger(reader, EXIF_COLOR_SPACE);
|
metadata.colorSpace = ReadInteger(reader, colorSpacePath);
|
||||||
|
|
||||||
// Extract author information
|
// Extract author information
|
||||||
metadata.author = ReadString(reader, EXIF_ARTIST);
|
metadata.author = ReadString(reader, artistPath);
|
||||||
metadata.copyright = ReadString(reader, EXIF_COPYRIGHT);
|
metadata.copyright = ReadString(reader, copyrightPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata)
|
void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat)
|
||||||
{
|
{
|
||||||
if (!reader)
|
if (!reader)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto lat = ReadMetadata(reader, GPS_LATITUDE);
|
// Select the correct paths based on container format
|
||||||
auto lon = ReadMetadata(reader, GPS_LONGITUDE);
|
const bool useIfdPaths = (pathFormat == MetadataPathFormat::IFD);
|
||||||
auto latRef = ReadMetadata(reader, GPS_LATITUDE_REF);
|
|
||||||
auto lonRef = ReadMetadata(reader, GPS_LONGITUDE_REF);
|
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)
|
if (lat && lon)
|
||||||
{
|
{
|
||||||
@@ -584,7 +678,7 @@ void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFM
|
|||||||
metadata.longitude = coords.second;
|
metadata.longitude = coords.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto alt = ReadMetadata(reader, GPS_ALTITUDE);
|
auto alt = ReadMetadata(reader, altitudePath);
|
||||||
if (alt)
|
if (alt)
|
||||||
{
|
{
|
||||||
metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get());
|
metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get());
|
||||||
|
|||||||
@@ -9,14 +9,32 @@
|
|||||||
#include <wincodec.h>
|
#include <wincodec.h>
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
|
|
||||||
|
// Forward declarations for unit test friend classes
|
||||||
|
namespace WICMetadataExtractorTests
|
||||||
|
{
|
||||||
|
class ExtractAVIFMetadataTests;
|
||||||
|
}
|
||||||
|
|
||||||
namespace PowerRenameLib
|
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>
|
/// <summary>
|
||||||
/// Windows Imaging Component (WIC) implementation for metadata extraction
|
/// Windows Imaging Component (WIC) implementation for metadata extraction
|
||||||
/// Provides efficient batch extraction of all metadata types with built-in caching
|
/// Provides efficient batch extraction of all metadata types with built-in caching
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class WICMetadataExtractor
|
class WICMetadataExtractor
|
||||||
{
|
{
|
||||||
|
// Friend declarations for unit testing
|
||||||
|
friend class WICMetadataExtractorTests::ExtractAVIFMetadataTests;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
WICMetadataExtractor();
|
WICMetadataExtractor();
|
||||||
~WICMetadataExtractor();
|
~WICMetadataExtractor();
|
||||||
@@ -45,10 +63,13 @@ namespace PowerRenameLib
|
|||||||
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
|
bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata);
|
||||||
|
|
||||||
// Batch extraction methods
|
// Batch extraction methods
|
||||||
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat);
|
||||||
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata);
|
void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata, MetadataPathFormat pathFormat);
|
||||||
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
|
void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata);
|
||||||
|
|
||||||
|
// Internal container format detection
|
||||||
|
MetadataPathFormat GetMetadataPathFormatFromDecoder(IWICBitmapDecoder* decoder);
|
||||||
|
|
||||||
// Field reading helpers
|
// Field reading helpers
|
||||||
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
|
std::optional<SYSTEMTIME> ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||||
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
|
std::optional<std::wstring> ReadString(IWICMetadataQueryReader* reader, const std::wstring& path);
|
||||||
|
|||||||
@@ -89,6 +89,12 @@
|
|||||||
<None Include="testdata\xmp_test_2.jpg">
|
<None Include="testdata\xmp_test_2.jpg">
|
||||||
<DeploymentContent>true</DeploymentContent>
|
<DeploymentContent>true</DeploymentContent>
|
||||||
</None>
|
</None>
|
||||||
|
<None Include="testdata\heif_test.heic">
|
||||||
|
<DeploymentContent>true</DeploymentContent>
|
||||||
|
</None>
|
||||||
|
<None Include="testdata\avif_test.avif">
|
||||||
|
<DeploymentContent>true</DeploymentContent>
|
||||||
|
</None>
|
||||||
<None Include="testdata\ATTRIBUTION.md">
|
<None Include="testdata\ATTRIBUTION.md">
|
||||||
<DeploymentContent>true</DeploymentContent>
|
<DeploymentContent>true</DeploymentContent>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@@ -227,18 +227,350 @@ namespace WICMetadataExtractorTests
|
|||||||
XMPMetadata metadata;
|
XMPMetadata metadata;
|
||||||
|
|
||||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||||
|
|
||||||
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
|
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||||
Assert::IsTrue(result1);
|
Assert::IsTrue(result1);
|
||||||
|
|
||||||
extractor.ClearCache();
|
extractor.ClearCache();
|
||||||
|
|
||||||
XMPMetadata metadata2;
|
XMPMetadata metadata2;
|
||||||
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
|
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
|
||||||
Assert::IsTrue(result2);
|
Assert::IsTrue(result2);
|
||||||
|
|
||||||
// Both calls should succeed
|
// Both calls should succeed
|
||||||
Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str());
|
Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TEST_CLASS(ExtractHEIFMetadataTests)
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_CameraInfo)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - camera information
|
||||||
|
// This test requires HEIF Image Extensions to be installed from Microsoft Store
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
// Check if file exists first
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
// If HEIF extension is not installed, extraction may fail
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify camera information from iPhone
|
||||||
|
Assert::IsTrue(metadata.cameraMake.has_value(), L"Camera make should be present");
|
||||||
|
Assert::AreEqual(L"Apple", metadata.cameraMake.value().c_str(), L"Camera make should be Apple");
|
||||||
|
|
||||||
|
Assert::IsTrue(metadata.cameraModel.has_value(), L"Camera model should be present");
|
||||||
|
// Model should contain "iPhone"
|
||||||
|
Assert::IsTrue(metadata.cameraModel.value().find(L"iPhone") != std::wstring::npos,
|
||||||
|
L"Camera model should contain iPhone");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_DateTaken)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - date taken
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify date taken is present
|
||||||
|
Assert::IsTrue(metadata.dateTaken.has_value(), L"Date taken should be present");
|
||||||
|
|
||||||
|
// Verify the date is a reasonable year (2020-2030 range)
|
||||||
|
SYSTEMTIME dt = metadata.dateTaken.value();
|
||||||
|
Assert::IsTrue(dt.wYear >= 2020 && dt.wYear <= 2030, L"Date taken year should be reasonable");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_ShootingParameters)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - shooting parameters
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify shooting parameters are present
|
||||||
|
Assert::IsTrue(metadata.iso.has_value(), L"ISO should be present");
|
||||||
|
Assert::IsTrue(metadata.iso.value() > 0, L"ISO should be positive");
|
||||||
|
|
||||||
|
Assert::IsTrue(metadata.aperture.has_value(), L"Aperture should be present");
|
||||||
|
Assert::IsTrue(metadata.aperture.value() > 0, L"Aperture should be positive");
|
||||||
|
|
||||||
|
Assert::IsTrue(metadata.shutterSpeed.has_value(), L"Shutter speed should be present");
|
||||||
|
Assert::IsTrue(metadata.shutterSpeed.value() > 0, L"Shutter speed should be positive");
|
||||||
|
|
||||||
|
Assert::IsTrue(metadata.focalLength.has_value(), L"Focal length should be present");
|
||||||
|
Assert::IsTrue(metadata.focalLength.value() > 0, L"Focal length should be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_GPS)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - GPS coordinates
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify GPS coordinates are present (if the test file has GPS data)
|
||||||
|
if (metadata.latitude.has_value() && metadata.longitude.has_value())
|
||||||
|
{
|
||||||
|
// Latitude should be between -90 and 90
|
||||||
|
Assert::IsTrue(metadata.latitude.value() >= -90.0 && metadata.latitude.value() <= 90.0,
|
||||||
|
L"Latitude should be valid");
|
||||||
|
|
||||||
|
// Longitude should be between -180 and 180
|
||||||
|
Assert::IsTrue(metadata.longitude.value() >= -180.0 && metadata.longitude.value() <= 180.0,
|
||||||
|
L"Longitude should be valid");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"GPS data not present in test file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_ImageDimensions)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - image dimensions
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify image dimensions are present
|
||||||
|
Assert::IsTrue(metadata.width.has_value(), L"Width should be present");
|
||||||
|
Assert::IsTrue(metadata.width.value() > 0, L"Width should be positive");
|
||||||
|
|
||||||
|
Assert::IsTrue(metadata.height.has_value(), L"Height should be present");
|
||||||
|
Assert::IsTrue(metadata.height.value() > 0, L"Height should be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractHEIF_EXIF_LensModel)
|
||||||
|
{
|
||||||
|
// Test HEIF EXIF extraction - lens model
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\heif_test.heic";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"HEIF extraction failed - HEIF Image Extensions may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"HEIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify lens model is present (iPhone photos typically have this)
|
||||||
|
if (metadata.lensModel.has_value())
|
||||||
|
{
|
||||||
|
Assert::IsFalse(metadata.lensModel.value().empty(), L"Lens model should not be empty");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"Lens model not present in test file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CLASS(ExtractAVIFMetadataTests)
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TEST_METHOD(ExtractAVIF_EXIF_CameraInfo)
|
||||||
|
{
|
||||||
|
// Test AVIF EXIF extraction - camera information
|
||||||
|
// This test requires AV1 Video Extension to be installed from Microsoft Store
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\avif_test.avif";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF extraction failed - AV1 Video Extension may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"AVIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify camera information
|
||||||
|
if (metadata.cameraMake.has_value())
|
||||||
|
{
|
||||||
|
Assert::IsFalse(metadata.cameraMake.value().empty(), L"Camera make should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.cameraModel.has_value())
|
||||||
|
{
|
||||||
|
Assert::IsFalse(metadata.cameraModel.value().empty(), L"Camera model should not be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractAVIF_EXIF_DateTaken)
|
||||||
|
{
|
||||||
|
// Test AVIF EXIF extraction - date taken
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\avif_test.avif";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF extraction failed - AV1 Video Extension may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"AVIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify date taken is present
|
||||||
|
if (metadata.dateTaken.has_value())
|
||||||
|
{
|
||||||
|
SYSTEMTIME dt = metadata.dateTaken.value();
|
||||||
|
Assert::IsTrue(dt.wYear >= 2000 && dt.wYear <= 2100, L"Date taken year should be reasonable");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"Date taken not present in AVIF test file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(ExtractAVIF_EXIF_ImageDimensions)
|
||||||
|
{
|
||||||
|
// Test AVIF EXIF extraction - image dimensions
|
||||||
|
WICMetadataExtractor extractor;
|
||||||
|
EXIFMetadata metadata;
|
||||||
|
|
||||||
|
std::wstring testFile = GetTestDataPath() + L"\\avif_test.avif";
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(testFile))
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF test file not found, skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = extractor.ExtractEXIFMetadata(testFile, metadata);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
Logger::WriteMessage(L"AVIF extraction failed - AV1 Video Extension may not be installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert::IsTrue(result, L"AVIF EXIF extraction should succeed");
|
||||||
|
|
||||||
|
// Verify image dimensions are present
|
||||||
|
if (metadata.width.has_value())
|
||||||
|
{
|
||||||
|
Assert::IsTrue(metadata.width.value() > 0, L"Width should be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.height.has_value())
|
||||||
|
{
|
||||||
|
Assert::IsTrue(metadata.height.value() > 0, L"Height should be positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/modules/powerrename/unittests/testdata/avif_test.avif
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/avif_test.avif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/modules/powerrename/unittests/testdata/heif_test.heic
vendored
Normal file
BIN
src/modules/powerrename/unittests/testdata/heif_test.heic
vendored
Normal file
Binary file not shown.
98
src/settings-ui/Settings.UI/Helpers/StoreExtensionHelper.cs
Normal file
98
src/settings-ui/Settings.UI/Helpers/StoreExtensionHelper.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Windows.Management.Deployment;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class to manage installation status and installation command for a Microsoft Store extension.
|
||||||
|
/// </summary>
|
||||||
|
public class StoreExtensionHelper : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private readonly string _packageFamilyName;
|
||||||
|
private readonly string _storeUri;
|
||||||
|
private readonly string _extensionName;
|
||||||
|
private bool? _isInstalled;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
public StoreExtensionHelper(string packageFamilyName, string storeUri, string extensionName)
|
||||||
|
{
|
||||||
|
_packageFamilyName = packageFamilyName ?? throw new ArgumentNullException(nameof(packageFamilyName));
|
||||||
|
_storeUri = storeUri ?? throw new ArgumentNullException(nameof(storeUri));
|
||||||
|
_extensionName = extensionName ?? throw new ArgumentNullException(nameof(extensionName));
|
||||||
|
InstallCommand = new AsyncCommand(InstallExtensionAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the extension is installed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInstalled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_isInstalled.HasValue)
|
||||||
|
{
|
||||||
|
_isInstalled = CheckExtensionInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _isInstalled.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command to install the extension.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand InstallCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the installation status of the extension.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshStatus()
|
||||||
|
{
|
||||||
|
_isInstalled = null;
|
||||||
|
OnPropertyChanged(nameof(IsInstalled));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckExtensionInstalled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var packageManager = new PackageManager();
|
||||||
|
var packages = packageManager.FindPackagesForUser(string.Empty, _packageFamilyName);
|
||||||
|
return packages.Any();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to check extension installation status: {_packageFamilyName}", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InstallExtensionAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Launcher.LaunchUriAsync(new Uri(_storeUri));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to open {_extensionName} extension store page", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,18 @@
|
|||||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||||
AutomationProperties.LandmarkType="Main"
|
AutomationProperties.LandmarkType="Main"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<local:NavigablePage.Resources>
|
||||||
|
<tkconverters:BoolToVisibilityConverter
|
||||||
|
x:Key="BoolToInvertedVisibilityConverter"
|
||||||
|
FalseValue="Visible"
|
||||||
|
TrueValue="Collapsed" />
|
||||||
|
</local:NavigablePage.Resources>
|
||||||
|
|
||||||
<controls:SettingsPageControl x:Uid="PowerRename" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerRename.png">
|
<controls:SettingsPageControl x:Uid="PowerRename" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerRename.png">
|
||||||
<controls:SettingsPageControl.ModuleContent>
|
<controls:SettingsPageControl.ModuleContent>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
@@ -97,6 +105,48 @@
|
|||||||
IsOn="{x:Bind ViewModel.UseBoostLib, Mode=TwoWay}" />
|
IsOn="{x:Bind ViewModel.UseBoostLib, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
|
<controls:SettingsGroup x:Uid="PowerRename_ExtensionsHeader" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
Name="PowerRenameHeifExtension"
|
||||||
|
x:Uid="PowerRename_HeifExtension"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind ViewModel.IsHeifExtensionInstalled, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
x:Uid="PowerRename_HeifExtension_Installed"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{x:Bind ViewModel.IsHeifExtensionInstalled, Mode=OneWay}" />
|
||||||
|
<Button
|
||||||
|
x:Uid="PowerRename_HeifExtension_Install"
|
||||||
|
Command="{x:Bind ViewModel.InstallHeifExtensionCommand}"
|
||||||
|
Visibility="{x:Bind ViewModel.IsHeifExtensionInstalled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
Name="PowerRenameAvifExtension"
|
||||||
|
x:Uid="PowerRename_AvifExtension"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<FontIcon
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||||
|
Glyph=""
|
||||||
|
Visibility="{x:Bind ViewModel.IsAvifExtensionInstalled, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
x:Uid="PowerRename_AvifExtension_Installed"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{x:Bind ViewModel.IsAvifExtensionInstalled, Mode=OneWay}" />
|
||||||
|
<Button
|
||||||
|
x:Uid="PowerRename_AvifExtension_Install"
|
||||||
|
Command="{x:Bind ViewModel.InstallAvifExtensionCommand}"
|
||||||
|
Visibility="{x:Bind ViewModel.IsAvifExtensionInstalled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
</controls:SettingsGroup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SettingsPageControl.ModuleContent>
|
</controls:SettingsPageControl.ModuleContent>
|
||||||
|
|
||||||
|
|||||||
@@ -1698,6 +1698,35 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
|||||||
<value>Provides extended features but may use different regex syntax</value>
|
<value>Provides extended features but may use different regex syntax</value>
|
||||||
<comment>Boost is a product name, should not be translated</comment>
|
<comment>Boost is a product name, should not be translated</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PowerRename_ExtensionsHeader.Header" xml:space="preserve">
|
||||||
|
<value>Extensions</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_HeifExtension.Header" xml:space="preserve">
|
||||||
|
<value>HEIF image metadata extraction</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_HeifExtension.Description" xml:space="preserve">
|
||||||
|
<value>Requires HEIF Image Extensions from Microsoft Store to extract metadata from HEIC/HEIF files</value>
|
||||||
|
<comment>HEIF is a file format name, do not translate</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_HeifExtension_Install.Content" xml:space="preserve">
|
||||||
|
<value>Install from Microsoft Store</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_HeifExtension_Installed.Text" xml:space="preserve">
|
||||||
|
<value>Installed</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_AvifExtension.Header" xml:space="preserve">
|
||||||
|
<value>AVIF image metadata extraction</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_AvifExtension.Description" xml:space="preserve">
|
||||||
|
<value>Requires AV1 Video Extension from Microsoft Store to extract metadata from AVIF files</value>
|
||||||
|
<comment>AVIF is a file format name, do not translate</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_AvifExtension_Install.Content" xml:space="preserve">
|
||||||
|
<value>Install from Microsoft Store</value>
|
||||||
|
</data>
|
||||||
|
<data name="PowerRename_AvifExtension_Installed.Text" xml:space="preserve">
|
||||||
|
<value>Installed</value>
|
||||||
|
</data>
|
||||||
<data name="MadeWithOssLove.Text" xml:space="preserve">
|
<data name="MadeWithOssLove.Text" xml:space="preserve">
|
||||||
<value>Made with 💗 by Microsoft and the PowerToys community.</value>
|
<value>Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
using global::PowerToys.GPOWrapper;
|
using global::PowerToys.GPOWrapper;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library;
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||||
@@ -67,6 +70,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
_autoComplete = Settings.Properties.MRUEnabled.Value;
|
_autoComplete = Settings.Properties.MRUEnabled.Value;
|
||||||
_powerRenameUseBoostLib = Settings.Properties.UseBoostLib.Value;
|
_powerRenameUseBoostLib = Settings.Properties.UseBoostLib.Value;
|
||||||
|
|
||||||
|
// Initialize extension helpers
|
||||||
|
HeifExtension = new StoreExtensionHelper(
|
||||||
|
"Microsoft.HEIFImageExtension_8wekyb3d8bbwe",
|
||||||
|
"ms-windows-store://pdp/?ProductId=9PMMSR1CGPWG",
|
||||||
|
"HEIF");
|
||||||
|
|
||||||
|
AvifExtension = new StoreExtensionHelper(
|
||||||
|
"Microsoft.AV1VideoExtension_8wekyb3d8bbwe",
|
||||||
|
"ms-windows-store://pdp/?ProductId=9MVZQVXJBQ9V",
|
||||||
|
"AV1");
|
||||||
|
|
||||||
InitializeEnabledValue();
|
InitializeEnabledValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,5 +284,31 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
OnPropertyChanged(nameof(IsEnabled));
|
OnPropertyChanged(nameof(IsEnabled));
|
||||||
OnPropertyChanged(nameof(GlobalAndMruEnabled));
|
OnPropertyChanged(nameof(GlobalAndMruEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store extension helpers
|
||||||
|
public StoreExtensionHelper HeifExtension { get; private set; }
|
||||||
|
|
||||||
|
public StoreExtensionHelper AvifExtension { get; private set; }
|
||||||
|
|
||||||
|
// Convenience properties for XAML binding
|
||||||
|
public bool IsHeifExtensionInstalled => HeifExtension.IsInstalled;
|
||||||
|
|
||||||
|
public bool IsAvifExtensionInstalled => AvifExtension.IsInstalled;
|
||||||
|
|
||||||
|
public ICommand InstallHeifExtensionCommand => HeifExtension.InstallCommand;
|
||||||
|
|
||||||
|
public ICommand InstallAvifExtensionCommand => AvifExtension.InstallCommand;
|
||||||
|
|
||||||
|
public void RefreshHeifExtensionStatus()
|
||||||
|
{
|
||||||
|
HeifExtension.RefreshStatus();
|
||||||
|
OnPropertyChanged(nameof(IsHeifExtensionInstalled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshAvifExtensionStatus()
|
||||||
|
{
|
||||||
|
AvifExtension.RefreshStatus();
|
||||||
|
OnPropertyChanged(nameof(IsAvifExtensionInstalled));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user