diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f063332bb8..8c2cff836e 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -143,6 +143,7 @@ DUPLICATEALIAS dustojnikhummer dvinns dwgs +dwrite ecfr ecfrbrowse EFGH diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 7cc2001c1d..2113127f0e 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -287,6 +287,11 @@ "description": "Enable support for the configure export command", "type": "boolean", "default": false + }, + "fonts": { + "description": "Enable support for managing fonts", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 58c83d1efb..45c9c0fa04 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -364,6 +364,7 @@ + @@ -410,6 +411,7 @@ + @@ -441,6 +443,7 @@ + @@ -490,6 +493,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 6c072ed000..a8f48edd34 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -260,6 +260,12 @@ Header Files + + Commands + + + Workflows + @@ -490,6 +496,12 @@ Source Files + + Commands + + + Workflows + diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index a5334a2ceb..0b1864df3c 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -198,6 +198,10 @@ namespace AppInstaller::CLI case Execution::Args::Type::IgnoreResumeLimit: return { type, "ignore-resume-limit"_liv, ArgTypeCategory::None }; + // Font command + case Execution::Args::Type::Family: + return { type, "family"_liv, ArgTypeCategory::None }; + // Configuration commands case Execution::Args::Type::ConfigurationFile: return { type, "file"_liv, 'f', ArgTypeCategory::ConfigurationSetChoice, ArgTypeExclusiveSet::ConfigurationSetChoice }; @@ -430,6 +434,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::ProxyArgumentDescription, ArgumentType::Standard, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions }; case Args::Type::NoProxy: return Argument{ type, Resource::String::NoProxyArgumentDescription, ArgumentType::Flag, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions }; + case Args::Type::Family: + return Argument{ type, Resource::String::FontFamilyNameArgumentDescription, ArgumentType::Positional, false }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/Commands/FontCommand.cpp b/src/AppInstallerCLICore/Commands/FontCommand.cpp new file mode 100644 index 0000000000..c6faae5b44 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/FontCommand.cpp @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "FontCommand.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Workflows/FontFlow.h" +#include "Resources.h" + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::CLI::Execution; + using namespace AppInstaller::CLI::Workflow; + using namespace AppInstaller::Utility::literals; + using namespace std::string_view_literals; + + Utility::LocIndView s_FontCommand_HelpLink = "https://aka.ms/winget-command-font"_liv; + + std::vector> FontCommand::GetCommands() const + { + return InitializeFromMoveOnly>>({ + std::make_unique(FullName()), + }); + } + + Resource::LocString FontCommand::ShortDescription() const + { + return { Resource::String::FontCommandShortDescription }; + } + + Resource::LocString FontCommand::LongDescription() const + { + return { Resource::String::FontCommandLongDescription }; + } + + Utility::LocIndView FontCommand::HelpLink() const + { + return s_FontCommand_HelpLink; + } + + void FontCommand::ExecuteInternal(Execution::Context& context) const + { + OutputHelp(context.Reporter); + } + + std::vector FontListCommand::GetArguments() const + { + return { + Argument::ForType(Args::Type::Family), + Argument::ForType(Args::Type::Moniker), + Argument::ForType(Args::Type::Source), + Argument::ForType(Args::Type::Tag), + Argument::ForType(Args::Type::Exact), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), + Argument::ForType(Args::Type::AcceptSourceAgreements), + }; + } + + Resource::LocString FontListCommand::ShortDescription() const + { + return { Resource::String::FontListCommandShortDescription }; + } + + Resource::LocString FontListCommand::LongDescription() const + { + return { Resource::String::FontListCommandLongDescription }; + } + + void FontListCommand::Complete(Execution::Context& context, Args::Type valueType) const + { + UNREFERENCED_PARAMETER(valueType); + context.Reporter.Error() << Resource::String::PendingWorkError << std::endl; + THROW_HR(E_NOTIMPL); + } + + Utility::LocIndView FontListCommand::HelpLink() const + { + return s_FontCommand_HelpLink; + } + + void FontListCommand::ExecuteInternal(Execution::Context& context) const + { + context << Workflow::ReportInstalledFonts; + } +} diff --git a/src/AppInstallerCLICore/Commands/FontCommand.h b/src/AppInstallerCLICore/Commands/FontCommand.h new file mode 100644 index 0000000000..c29f703f6c --- /dev/null +++ b/src/AppInstallerCLICore/Commands/FontCommand.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" +#include + +namespace AppInstaller::CLI +{ + struct FontCommand final : public Command + { + FontCommand(std::string_view parent) : Command("font", { "fonts" }, parent, Settings::ExperimentalFeature::Feature::Font) {} + + std::vector> GetCommands() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; + + struct FontListCommand final : public Command + { + FontListCommand(std::string_view parent) : Command("list", parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp index f5a61d79fa..5580efa14a 100644 --- a/src/AppInstallerCLICore/Commands/RootCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -15,6 +15,7 @@ #include "ValidateCommand.h" #include "SettingsCommand.h" #include "FeaturesCommand.h" +#include "FontCommand.h" #include "ExperimentalCommand.h" #include "CompleteCommand.h" #include "ExportCommand.h" @@ -194,6 +195,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), #if _DEBUG std::make_unique(FullName()), #endif diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 41739b36f4..7bb2c28628 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -121,6 +121,9 @@ namespace AppInstaller::CLI::Execution ResumeId, IgnoreResumeLimit, + // Font Command + Family, + // Configuration ConfigurationFile, ConfigurationAcceptWarning, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 0e8407ab2e..5aa7616509 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -248,6 +248,16 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(FileNotFound); WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError); + WINGET_DEFINE_RESOURCE_STRINGID(FontCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontFace); + WINGET_DEFINE_RESOURCE_STRINGID(FontFaces); + WINGET_DEFINE_RESOURCE_STRINGID(FontFamily); + WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths); + WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(FontVersion); WINGET_DEFINE_RESOURCE_STRINGID(ForceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(GatedVersionArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(GetManifestResultVersionNotFound); @@ -403,6 +413,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoAdminRepairForUserScopePackage); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); + WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledFontFound); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageSelectionArgumentProvided); diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp new file mode 100644 index 0000000000..ce9bb7a233 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "FontFlow.h" +#include "TableOutput.h" +#include +#include + +namespace AppInstaller::CLI::Workflow +{ + using namespace AppInstaller::CLI::Execution; + + namespace + { + struct InstalledFontFamiliesTableLine + { + InstalledFontFamiliesTableLine(Utility::LocIndString familyName, int faceCount) + : FamilyName(familyName), FaceCount(faceCount) {} + + Utility::LocIndString FamilyName; + int FaceCount; + }; + + struct InstalledFontFacesTableLine + { + InstalledFontFacesTableLine(Utility::LocIndString familyName, Utility::LocIndString faceName, Utility::LocIndString faceVersion, std::filesystem::path filePath) + : FamilyName(familyName), FaceName(faceName), FaceVersion(faceVersion), FilePath(filePath) {} + + Utility::LocIndString FamilyName; + Utility::LocIndString FaceName; + Utility::LocIndString FaceVersion; + std::filesystem::path FilePath; + }; + + void OutputInstalledFontFamiliesTable(Execution::Context& context, const std::vector& lines) + { + Execution::TableOutput<2> table(context.Reporter, { Resource::String::FontFamily, Resource::String::FontFaces }); + + for (auto line : lines) + { + table.OutputLine({ line.FamilyName, std::to_string(line.FaceCount) }); + } + + table.Complete(); + } + + void OutputInstalledFontFacesTable(Execution::Context& context, const std::vector& lines) + { + Execution::TableOutput<4> table(context.Reporter, { Resource::String::FontFamily, Resource::String::FontFace, Resource::String::FontVersion, Resource::String::FontFilePaths }); + + bool anonymizePath = Settings::User().Get(); + + for (auto line : lines) + { + if (anonymizePath) + { + AppInstaller::Runtime::ReplaceProfilePathsWithEnvironmentVariable(line.FilePath); + } + + table.OutputLine({ line.FamilyName, line.FaceName, line.FaceVersion, line.FilePath.u8string() }); + } + + table.Complete(); + } + } + + void ReportInstalledFonts(Execution::Context& context) + { + Fonts::FontCatalog fontCatalog; + + if (context.Args.Contains(Args::Type::Family)) + { + // TODO: Create custom source and search mechanism for fonts. + const auto& familyNameArg = AppInstaller::Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::Family)); + const auto& fontFamilies = fontCatalog.GetInstalledFontFamilies(familyNameArg); + + if (fontFamilies.empty()) + { + context.Reporter.Info() << Resource::String::NoInstalledFontFound << std::endl; + return; + } + + std::vector lines; + + for (const auto& fontFamily : fontFamilies) + { + const auto& familyName = Utility::LocIndString(Utility::ConvertToUTF8(fontFamily.Name)); + + for (const auto& fontFace : fontFamily.Faces) + { + for (const auto& filePath : fontFace.FilePaths) + { + InstalledFontFacesTableLine line( + familyName, + Utility::LocIndString(Utility::ToLower(Utility::ConvertToUTF8(fontFace.Name))), + Utility::LocIndString(fontFace.Version.ToString()), + filePath.u8string()); + + lines.push_back(std::move(line)); + } + } + } + + OutputInstalledFontFacesTable(context, lines); + } + else + { + const auto& fontFamilies = fontCatalog.GetInstalledFontFamilies(); + std::vector lines; + + for (const auto& fontFamily : fontFamilies) + { + InstalledFontFamiliesTableLine line( + Utility::LocIndString(Utility::ConvertToUTF8(fontFamily.Name)), + static_cast(fontFamily.Faces.size()) + ); + + lines.push_back(std::move(line)); + } + + OutputInstalledFontFamiliesTable(context, lines); + } + } +} diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.h b/src/AppInstallerCLICore/Workflows/FontFlow.h new file mode 100644 index 0000000000..c3346e1a60 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/FontFlow.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Reports the installed fonts as a table. + // Required Args: None + // Inputs: None + // Outputs: None + void ReportInstalledFonts(Execution::Context& context); +} diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs index 1b28227fd5..faa07c2dd0 100644 --- a/src/AppInstallerCLIE2ETests/BaseCommand.cs +++ b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,10 +6,7 @@ namespace AppInstallerCLIE2ETests { - using System; - using System.IO; using AppInstallerCLIE2ETests.Helpers; - using Newtonsoft.Json.Linq; using NUnit.Framework; /// diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 9f0f88b8df..46d093b80c 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3138,5 +3138,40 @@ Please specify one of them using the --source option to proceed. Downloaded zero byte installer; ensure that your network connection is working properly. + + + Manage fonts + + + Manage fonts with sub-commands. Fonts can be installed, upgraded, or uninstalled similar to packages. + + + Family + + + Faces + "Faces" represents the typeface of the font family such as 'Bold' or 'Italic' + + + Filter results by family name + + + Face + "Face" represents the typeface of a font family such as 'Bold' or 'Italic' + + + Paths + + + No installed font found matching input criteria. + + + List installed fonts + + + List all installed fonts, or full details of a specific font. + + + Version \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 043eab3f5a..7500b2c1bb 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -284,6 +284,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index a24784a802..5d28140d17 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -368,6 +368,9 @@ Source Files\Repository + + Source Files\Common + diff --git a/src/AppInstallerCLITests/Fonts.cpp b/src/AppInstallerCLITests/Fonts.cpp new file mode 100644 index 0000000000..81a50791e8 --- /dev/null +++ b/src/AppInstallerCLITests/Fonts.cpp @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include + +using namespace AppInstaller::Fonts; +using namespace TestCommon; + +constexpr std::wstring_view s_testFontName = L"Times New Roman"; + +TEST_CASE("GetInstalledFonts", "[fonts]") +{ + FontCatalog fontCatalog; + std::vector installedFontFamilies; + REQUIRE_NOTHROW(installedFontFamilies = fontCatalog.GetInstalledFontFamilies()); + REQUIRE(installedFontFamilies.size() > 0); +} + +TEST_CASE("GetSingleFontFamily", "[fonts]") +{ + FontCatalog fontCatalog; + std::vector fontFamily; + REQUIRE_NOTHROW(fontFamily = fontCatalog.GetInstalledFontFamilies(std::wstring(s_testFontName))); + REQUIRE_FALSE(fontFamily.empty()); + FontFamily singleFontFamily = fontFamily[0]; + REQUIRE(AppInstaller::Utility::CaseInsensitiveEquals(singleFontFamily.Name, s_testFontName)); + REQUIRE(singleFontFamily.Faces.size() > 0); +} + +TEST_CASE("GetInvalidFontFamily", "[fonts]") +{ + FontCatalog fontCatalog; + std::vector fontFamily; + REQUIRE_NOTHROW(fontFamily = fontCatalog.GetInstalledFontFamilies(L"Invalid Font")); + REQUIRE(fontFamily.empty()); +} diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp index 0988d58074..54975eda49 100644 --- a/src/AppInstallerCLITests/Versions.cpp +++ b/src/AppInstallerCLITests/Versions.cpp @@ -421,3 +421,30 @@ TEST_CASE("SemanticVersion", "[versions]") REQUIRE(version.BuildMetadata() == Version("4.5.6")); REQUIRE(version.PartAt(2).Other == "-beta+4.5.6"); } + +TEST_CASE("OpenTypeFontVersion", "[versions]") +{ + // Valid font version. + OpenTypeFontVersion version = OpenTypeFontVersion("Version 1.234"); + REQUIRE(version.ToString() == "1.234"); + REQUIRE(version.GetParts().size() == 2); + REQUIRE(version.PartAt(0).Integer == 1); + REQUIRE(version.PartAt(1).Integer == 234); + + // Font version with additional metadata. + version = OpenTypeFontVersion("Version 9.876.54 ;2024"); + REQUIRE(version.ToString() == "9.876"); + REQUIRE(version.GetParts().size() == 2); + REQUIRE(version.PartAt(0).Integer == 9); + REQUIRE(version.PartAt(1).Integer == 876); + + // Invalid version. Font version must have at least 2 parts. + REQUIRE_NOTHROW(version = OpenTypeFontVersion("1234567")); + REQUIRE(version.IsUnknown()); + REQUIRE(version.ToString() == "Unknown"); + + // Major and minor parts must have digits. + REQUIRE_NOTHROW(version = OpenTypeFontVersion(" abc.def ")); + REQUIRE(version.IsUnknown()); + REQUIRE(version.ToString() == "Unknown"); +} diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index d57fcfc5e6..927547fc8f 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -427,6 +427,7 @@ + true @@ -478,6 +479,7 @@ + true diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index 3498d88dfb..1cb64a4d0e 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -204,6 +204,9 @@ Public\winget + + Public\winget + @@ -368,6 +371,9 @@ Source Files + + Source Files + diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 733ff9ccd5..c3709c6802 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -48,6 +48,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::ConfigureExport: return userSettings.Get(); + case ExperimentalFeature::Feature::Font: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -85,6 +87,9 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Configure Self Elevation", "configureSelfElevate", "https://aka.ms/winget-settings", Feature::ConfigureSelfElevation }; case Feature::ConfigureExport: return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport }; + case Feature::Font: + return ExperimentalFeature{ "Font", "Font", "https://aka.ms/winget-settings", Feature::Font }; + default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Fonts.cpp b/src/AppInstallerCommonCore/Fonts.cpp new file mode 100644 index 0000000000..6f5f328f93 --- /dev/null +++ b/src/AppInstallerCommonCore/Fonts.cpp @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include +#include +#include + +namespace AppInstaller::Fonts +{ + namespace + { + std::vector GetFontFilePaths(const wil::com_ptr& fontFace) + { + UINT32 fileCount = 0; + THROW_IF_FAILED(fontFace->GetFiles(&fileCount, nullptr)); + + static_assert(sizeof(wil::com_ptr) == sizeof(IDWriteFontFile*)); + std::vector> fontFiles; + fontFiles.resize(fileCount); + + THROW_IF_FAILED(fontFace->GetFiles(&fileCount, fontFiles[0].addressof())); + + std::vector filePaths; + for (UINT32 i = 0; i < fileCount; ++i) { + wil::com_ptr loader; + THROW_IF_FAILED(fontFiles[i]->GetLoader(loader.addressof())); + + const void* fontFileReferenceKey; + UINT32 fontFileReferenceKeySize; + THROW_IF_FAILED(fontFiles[i]->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize)); + + if (const auto localLoader = loader.try_query()) { + UINT32 pathLength; + THROW_IF_FAILED(localLoader->GetFilePathLengthFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &pathLength)); + pathLength += 1; // Account for the trailing null terminator during allocation. + + std::wstring path; + path.resize(pathLength); + THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &path[0], pathLength)); + path.resize(pathLength - 1); // Remove the null char. + filePaths.emplace_back(std::move(path)); + } + } + + return filePaths; + } + } + + FontCatalog::FontCatalog() + { + m_preferredLocales = AppInstaller::Locale::GetUserPreferredLanguagesUTF16(); + } + + std::vector FontCatalog::GetInstalledFontFamilies(std::optional familyName) + { + wil::com_ptr factory; + THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), factory.put_unknown())); + + wil::com_ptr collection; + THROW_IF_FAILED(factory->GetSystemFontCollection(collection.addressof(), FALSE)); + + std::vector installedFontFamilies; + + if (familyName.has_value()) + { + UINT32 index; + BOOL exists; + THROW_IF_FAILED(collection->FindFamilyName(familyName.value().c_str(), &index, &exists)); + + if (exists) + { + installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index)); + } + } + else + { + UINT32 familyCount = collection->GetFontFamilyCount(); + + for (UINT32 index = 0; index < familyCount; index++) + { + installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index)); + } + } + + return installedFontFamilies; + } + + std::wstring FontCatalog::GetLocalizedStringFromFont(const wil::com_ptr& localizedStringCollection) + { + UINT32 index = 0; + BOOL exists = false; + + for (const auto& locale : m_preferredLocales) + { + if (SUCCEEDED_LOG(localizedStringCollection->FindLocaleName(locale.c_str(), &index, &exists)) && exists) + { + break; + } + } + + // If the locale does not exist, resort to the default value at the 0 index. + if (!exists) + { + index = 0; + } + + UINT32 length = 0; + THROW_IF_FAILED(localizedStringCollection->GetStringLength(index, &length)); + length += 1; // Account for the trailing null terminator during allocation. + + std::wstring localizedString; + localizedString.resize(length); + THROW_IF_FAILED(localizedStringCollection->GetString(index, &localizedString[0], length)); + localizedString.resize(length - 1); // Remove the null char. + return localizedString; + } + + std::wstring FontCatalog::GetFontFaceName(const wil::com_ptr& font) + { + wil::com_ptr faceNames; + THROW_IF_FAILED(font->GetFaceNames(faceNames.addressof())); + return GetLocalizedStringFromFont(faceNames); + } + + std::wstring FontCatalog::GetFontFamilyName(const wil::com_ptr& fontFamily) + { + wil::com_ptr familyNames; + THROW_IF_FAILED(fontFamily->GetFamilyNames(familyNames.addressof())); + return GetLocalizedStringFromFont(familyNames); + } + + Utility::OpenTypeFontVersion FontCatalog::GetFontFaceVersion(const wil::com_ptr& font) + { + wil::com_ptr fontVersion; + BOOL exists; + THROW_IF_FAILED(font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_VERSION_STRINGS, fontVersion.addressof(), &exists)); + if (!exists) + { + return {}; + } + + std::string value = AppInstaller::Utility::ConvertToUTF8(GetLocalizedStringFromFont(fontVersion)); + Utility::OpenTypeFontVersion openTypeFontVersion{ value }; + return openTypeFontVersion; + } + + FontFamily FontCatalog::GetFontFamilyByIndex(const wil::com_ptr& collection, UINT32 index) + { + wil::com_ptr family; + THROW_IF_FAILED(collection->GetFontFamily(index, family.addressof())); + std::wstring familyName = GetFontFamilyName(family); + + std::vector fontFaces; + UINT32 fontCount = family->GetFontCount(); + for (UINT32 fontIndex = 0; fontIndex < fontCount; fontIndex++) + { + wil::com_ptr font; + THROW_IF_FAILED(family->GetFont(fontIndex, font.addressof())); + + wil::com_ptr fontFace; + THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof())); + + FontFace fontFaceEntry; + fontFaceEntry.Name = GetFontFaceName(font); + fontFaceEntry.Version = GetFontFaceVersion(font); + fontFaceEntry.FilePaths = GetFontFilePaths(fontFace); + fontFaces.emplace_back(std::move(fontFaceEntry)); + } + + FontFamily fontFamily; + fontFamily.Name = std::move(familyName); + fontFamily.Faces = std::move(fontFaces); + return fontFamily; + } +} diff --git a/src/AppInstallerCommonCore/Locale.cpp b/src/AppInstallerCommonCore/Locale.cpp index c308584226..ae8985910c 100644 --- a/src/AppInstallerCommonCore/Locale.cpp +++ b/src/AppInstallerCommonCore/Locale.cpp @@ -120,6 +120,18 @@ namespace AppInstaller::Locale return result; } + std::vector GetUserPreferredLanguagesUTF16() + { + std::vector result; + + for (const auto& lang : winrt::Windows::System::UserProfile::GlobalizationPreferences::Languages()) + { + result.emplace_back(std::wstring(lang)); + } + + return result; + } + std::string LocaleIdToBcp47Tag(LCID localeId) { WCHAR localeName[MAX_LOCALE_SNAME_LEN] = {0}; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h index 2b8a729a2d..cecadb24d8 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -56,6 +56,10 @@ namespace AppInstaller::Runtime CLIExecutable, // The location of the image assets, if it exists. ImageAssets, + // The location where fonts are installed with user scope. + FontsUserInstallLocation, + // The location where fonts are installed with machine scope. + FontsMachineInstallLocation, // Always one more than the last path; for being able to iterate paths in tests. Max }; @@ -70,6 +74,9 @@ namespace AppInstaller::Runtime return Filesystem::GetPathTo(path, forDisplay); } + // Replaces the substring in the path with the user profile environment variable. + void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path); + // Gets a new temp file path. std::filesystem::path GetNewTempFilePath(); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 733f1aa453..80469aa24c 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,6 +27,7 @@ namespace AppInstaller::Settings Configuration03 = 0x4, ConfigureSelfElevation = 0x8, ConfigureExport = 0x10, + Font = 0x20, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/Fonts.h b/src/AppInstallerCommonCore/Public/winget/Fonts.h new file mode 100644 index 0000000000..443dea5f7a --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/Fonts.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include + +namespace AppInstaller::Fonts +{ + struct FontFace + { + std::wstring Name; + std::vector FilePaths; + Utility::OpenTypeFontVersion Version; + }; + + struct FontFamily + { + std::wstring Name; + std::vector Faces; + }; + + struct FontCatalog + { + FontCatalog(); + + // Gets all installed font families on the system. If an exact family name is provided and found, returns the installed font family. + std::vector GetInstalledFontFamilies(std::optional familyName = {}); + + private: + FontFamily GetFontFamilyByIndex(const wil::com_ptr& collection, UINT32 index); + std::wstring GetLocalizedStringFromFont(const wil::com_ptr& localizedStringCollection); + std::wstring GetFontFamilyName(const wil::com_ptr& fontFamily); + std::wstring GetFontFaceName(const wil::com_ptr& font); + Utility::OpenTypeFontVersion GetFontFaceVersion(const wil::com_ptr& font); + + std::vector m_preferredLocales; + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/Locale.h b/src/AppInstallerCommonCore/Public/winget/Locale.h index f4cd7a2d54..5ae86a8de4 100644 --- a/src/AppInstallerCommonCore/Public/winget/Locale.h +++ b/src/AppInstallerCommonCore/Public/winget/Locale.h @@ -20,6 +20,9 @@ namespace AppInstaller::Locale // Get the list of user Preferred Languages from settings. Returns an empty vector in rare cases of failure. std::vector GetUserPreferredLanguages(); + // Get the list of user Preferred Languages from settings. Returns an empty vector in rare cases of failure. + std::vector GetUserPreferredLanguagesUTF16(); + // Get the bcp47 tag from a locale id. Returns empty string if conversion cannot be performed. std::string LocaleIdToBcp47Tag(LCID localeId); -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 5d8615485a..b580775c28 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -78,6 +78,7 @@ namespace AppInstaller::Settings EFConfiguration03, EFConfigureSelfElevation, EFConfigureExport, + EFFonts, // Telemetry TelemetryDisable, // Install behavior @@ -161,6 +162,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureSelfElevation, bool, bool, false, ".experimentalFeatures.configureSelfElevate"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp index 92bb493a83..3a8973d8e9 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -29,6 +29,7 @@ namespace AppInstaller::Runtime constexpr std::string_view s_PortablePackageRoot = "WinGet"sv; constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv; constexpr std::string_view s_LinksDirectory = "Links"sv; + constexpr std::string_view s_FontsInstallDirectory = "Microsoft\\Windows\\Fonts"sv; // Use production CLSIDs as a surrogate for repository location. #if USE_PROD_CLSIDS constexpr std::string_view s_ImageAssetsDirectoryRelative = "Assets\\WinGet"sv; @@ -162,15 +163,6 @@ namespace AppInstaller::Runtime return result; } - - // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE. - void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path) - { - if (!ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_LocalAppData), s_LocalAppDataEnvironmentVariable)) - { - ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_Profile), s_UserProfileEnvironmentVariable); - } - } } void SetRuntimePathStateName(std::string name) @@ -240,6 +232,14 @@ namespace AppInstaller::Runtime result.Path = GetKnownFolderPath(FOLDERID_Downloads); mayBeInProfilePath = true; break; + case PathName::FontsUserInstallLocation: + result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); + result.Path /= s_FontsInstallDirectory; + mayBeInProfilePath = true; + break; + case PathName::FontsMachineInstallLocation: + result.Path = GetKnownFolderPath(FOLDERID_Fonts); + break; default: THROW_HR(E_UNEXPECTED); } @@ -314,6 +314,8 @@ namespace AppInstaller::Runtime case PathName::PortableLinksUserLocation: case PathName::PortablePackageUserRoot: case PathName::UserProfileDownloads: + case PathName::FontsUserInstallLocation: + case PathName::FontsMachineInstallLocation: result = GetPathDetailsCommon(path, forDisplay); break; case PathName::SelfPackageRoot: @@ -418,6 +420,8 @@ namespace AppInstaller::Runtime case PathName::PortableLinksUserLocation: case PathName::PortablePackageUserRoot: case PathName::UserProfileDownloads: + case PathName::FontsUserInstallLocation: + case PathName::FontsMachineInstallLocation: result = GetPathDetailsCommon(path, forDisplay); break; case PathName::SelfPackageRoot: @@ -476,6 +480,15 @@ namespace AppInstaller::Runtime return result; } + // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE. + void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path) + { + if (!ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_LocalAppData), s_LocalAppDataEnvironmentVariable)) + { + ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_Profile), s_UserProfileEnvironmentVariable); + } + } + std::filesystem::path GetNewTempFilePath() { GUID guid; diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 476971d2f0..9e13610537 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -269,6 +269,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03) WINGET_VALIDATE_PASS_THROUGH(EFConfigureSelfElevation) WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport) + WINGET_VALIDATE_PASS_THROUGH(EFFonts) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h index 569a33aa32..9df7cab413 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h @@ -281,4 +281,20 @@ namespace AppInstaller::Utility // Checks if there are overlaps within the list of version ranges bool HasOverlapInVersionRanges(const std::vector& ranges); + + // The OpenType font version. + // The format of this version type is 'Version 1.234 ;567' + // The only part that is of importance is the 'Major.Minor' parts. + // The 'Version' string is typically found at the beginning of the version string. + // Any value after a digit that is not a '.' represents some other meaning. + struct OpenTypeFontVersion : Version + { + OpenTypeFontVersion() = default; + + OpenTypeFontVersion(std::string&& version); + OpenTypeFontVersion(const std::string& version) : + OpenTypeFontVersion(std::string(version)) {} + + void Assign(std::string version, std::string_view splitChars = DefaultSplitChars) override; + }; } diff --git a/src/AppInstallerSharedLib/Versions.cpp b/src/AppInstallerSharedLib/Versions.cpp index 6b02675d1d..2c9972c561 100644 --- a/src/AppInstallerSharedLib/Versions.cpp +++ b/src/AppInstallerSharedLib/Versions.cpp @@ -661,4 +661,64 @@ namespace AppInstaller::Utility return false; } + + OpenTypeFontVersion::OpenTypeFontVersion(std::string&& version) + { + Assign(std::move(version), DefaultSplitChars); + } + + void OpenTypeFontVersion::Assign(std::string version, std::string_view splitChars) + { + // Open type version requires using the default split character + THROW_HR_IF(E_INVALIDARG, splitChars != DefaultSplitChars); + + // Split on default split character. + std::vector parts = Split(version, '.', true); + + std::string majorString; + std::string minorString; + + // Font version must have a "major.minor" part. + if (parts.size() >= 2) + { + // Find first digit and trim all preceding characters. + std::string firstPart = parts[0]; + size_t majorStartIndex = firstPart.find_first_of(s_Digit_Characters); + + if (majorStartIndex != std::string::npos) + { + firstPart.erase(0, majorStartIndex); + } + + size_t majorEndIndex = firstPart.find_last_of(s_Digit_Characters); + majorString = firstPart.substr(0, majorEndIndex + 1); + + // Parse and verify minor part. + std::string secondPart = parts[1]; + size_t endPos = secondPart.find_first_not_of(s_Digit_Characters); + + // If a non-digit character exists, trim off the remainder. + if (endPos != std::string::npos) + { + secondPart.erase(endPos, secondPart.length()); + } + + minorString = secondPart; + } + + // Verify results. + if (!majorString.empty() && !minorString.empty()) + { + m_parts.emplace_back(majorString); + m_parts.emplace_back(minorString); + m_version = Utility::Join(DefaultSplitChars, { majorString, minorString }); + + Trim(); + } + else + { + m_version = s_Version_Part_Unknown; + m_parts.emplace_back(0, std::string{ s_Version_Part_Unknown }); + } + } }