mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-11 05:52:19 +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
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\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:
|
||||
// - 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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
<None Include="testdata\xmp_test_2.jpg">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</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">
|
||||
<DeploymentContent>true</DeploymentContent>
|
||||
</None>
|
||||
|
||||
@@ -227,18 +227,350 @@ namespace WICMetadataExtractorTests
|
||||
XMPMetadata metadata;
|
||||
|
||||
std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg";
|
||||
|
||||
|
||||
bool result1 = extractor.ExtractXMPMetadata(testFile, metadata);
|
||||
Assert::IsTrue(result1);
|
||||
|
||||
|
||||
extractor.ClearCache();
|
||||
|
||||
|
||||
XMPMetadata metadata2;
|
||||
bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2);
|
||||
Assert::IsTrue(result2);
|
||||
|
||||
|
||||
// Both calls should succeed
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
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.ModuleContent>
|
||||
<StackPanel
|
||||
@@ -97,6 +105,48 @@
|
||||
IsOn="{x:Bind ViewModel.UseBoostLib, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</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>
|
||||
</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>
|
||||
<comment>Boost is a product name, should not be translated</comment>
|
||||
</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">
|
||||
<value>Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
</data>
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
@@ -67,6 +70,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_autoComplete = Settings.Properties.MRUEnabled.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();
|
||||
}
|
||||
|
||||
@@ -270,5 +284,31 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
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