Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions change/react-native-windows-fix-app-bundled-fonts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Fabric: resolve app-bundled fonts (Assets, Assets\\Fonts) during text layout by merging them with the system font set into the DirectWrite font collection used by WindowsTextLayoutManager",
"packageName": "react-native-windows",
"email": "collindanielschneide@gmail.com",
"dependentChangeType": "patch"
}
104 changes: 104 additions & 0 deletions vnext/Microsoft.ReactNative/Fabric/DWriteHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

#include "DWriteHelpers.h"

#include <dwrite_3.h>
#include <windows.h>
#include <cstdint>
#include <string>

namespace Microsoft::ReactNative {

winrt::com_ptr<::IDWriteFactory> DWriteFactory() noexcept {
Expand All @@ -16,4 +21,103 @@ winrt::com_ptr<::IDWriteFactory> DWriteFactory() noexcept {
return s_dwriteFactory;
}

namespace {

// Directory that contains the running module, including the trailing separator: the
// package root for packaged (MSIX) apps and the directory next to the .exe for
// unpackaged apps. Bundled font assets are deployed below this directory.
std::wstring AppDirectory() noexcept {
wchar_t modulePath[MAX_PATH]{};
const DWORD length = ::GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return {};
}
std::wstring path(modulePath, length);
const auto lastSeparator = path.find_last_of(L"\\/");
if (lastSeparator == std::wstring::npos) {
return {};
}
path.resize(lastSeparator + 1);
return path;
}

// Adds every file matching <directory> + <pattern> to the font-set builder and returns
// the number of files added. Per-file failures are skipped so that one bad font file
// cannot break font resolution for the rest of the app.
uint32_t AddFontFiles(
::IDWriteFactory5 *factory,
::IDWriteFontSetBuilder1 *builder,
const std::wstring &directory,
const wchar_t *pattern) noexcept {
uint32_t count = 0;
const std::wstring searchPattern = directory + pattern;
WIN32_FIND_DATAW findData{};
const HANDLE findHandle = ::FindFirstFileW(searchPattern.c_str(), &findData);
if (findHandle == INVALID_HANDLE_VALUE) {
return count;
}
do {
if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
const std::wstring fontPath = directory + findData.cFileName;
winrt::com_ptr<::IDWriteFontFile> fontFile;
if (SUCCEEDED(factory->CreateFontFileReference(fontPath.c_str(), nullptr, fontFile.put())) &&
SUCCEEDED(builder->AddFontFile(fontFile.get()))) {
++count;
}
}
} while (::FindNextFileW(findHandle, &findData));
::FindClose(findHandle);
return count;
}

winrt::com_ptr<::IDWriteFontCollection> CreateAppFontCollection() noexcept {
try {
const std::wstring appDirectory = AppDirectory();
if (appDirectory.empty()) {
return nullptr;
}

const auto factory5 = DWriteFactory().as<::IDWriteFactory5>();

winrt::com_ptr<::IDWriteFontSetBuilder1> builder;
winrt::check_hresult(factory5->CreateFontSetBuilder(builder.put()));

// Include the system font set so that system families keep resolving when this
// collection is used in place of the system collection.
winrt::com_ptr<::IDWriteFontSet> systemFontSet;
winrt::check_hresult(factory5->GetSystemFontSet(systemFontSet.put()));
winrt::check_hresult(builder->AddFontSet(systemFontSet.get()));

uint32_t fontFileCount = 0;
for (const auto *subdirectory : {L"Assets\\", L"Assets\\Fonts\\"}) {
for (const auto *pattern : {L"*.ttf", L"*.otf"}) {
fontFileCount += AddFontFiles(factory5.get(), builder.get(), appDirectory + subdirectory, pattern);
}
}
if (fontFileCount == 0) {
// Nothing bundled: report "no app collection" so callers pass nullptr to DirectWrite
// and keep using DirectWrite's own (cached, updatable) system font collection.
return nullptr;
}

winrt::com_ptr<::IDWriteFontSet> fontSet;
winrt::check_hresult(builder->CreateFontSet(fontSet.put()));
winrt::com_ptr<::IDWriteFontCollection1> collection;
winrt::check_hresult(factory5->CreateFontCollectionFromFontSet(fontSet.get(), collection.put()));
return collection.as<::IDWriteFontCollection>();
} catch (...) {
// Fail closed: callers fall back to the system font collection (previous behavior).
return nullptr;
}
}

} // namespace

winrt::com_ptr<::IDWriteFontCollection> DWriteAppFontCollection() noexcept {
// Thread-safe (magic static) one-time initialization. Bundled font assets cannot
// change for the lifetime of the process, so the collection never needs rebuilding.
static const winrt::com_ptr<::IDWriteFontCollection> s_appFontCollection = CreateAppFontCollection();
return s_appFontCollection;
}

} // namespace Microsoft::ReactNative
7 changes: 7 additions & 0 deletions vnext/Microsoft.ReactNative/Fabric/DWriteHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@ namespace Microsoft::ReactNative {

winrt::com_ptr<::IDWriteFactory> DWriteFactory() noexcept;

// Font collection that merges the system font set with every font file bundled in the
// application's Assets\ and Assets\Fonts\ directories (*.ttf / *.otf), so app-bundled
// font families resolve during text layout exactly like installed fonts. Built once on
// first use. Returns nullptr when the app bundles no fonts or when the collection
// cannot be built; callers should treat nullptr as "use the system font collection".
winrt::com_ptr<::IDWriteFontCollection> DWriteAppFontCollection() noexcept;

} // namespace Microsoft::ReactNative
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ void WindowsTextLayoutManager::GetTextLayout(
outerFragment.textAttributes.fontFamily.empty()
? L"Segoe UI"
: Microsoft::Common::Unicode::Utf8ToUtf16(outerFragment.textAttributes.fontFamily).c_str(),
nullptr, // Font collection (nullptr sets it to use the system font collection).
// Bundled app fonts merged over the system font set (nullptr when the app bundles
// no fonts, which selects the system font collection as before).
Microsoft::ReactNative::DWriteAppFontCollection().get(),
static_cast<DWRITE_FONT_WEIGHT>(outerFragment.textAttributes.fontWeight.value_or(
static_cast<facebook::react::FontWeight>(DWRITE_FONT_WEIGHT_REGULAR))),
style,
Expand Down
Loading