Rework Windows font collector (arch1t3cht/Aegisub#107)

[src\meson.build] Add DirectWrite has dependency

[src\font_file_lister_gdi] Rework GDI FontCollector to use DirectWrite

This replaces all the logic of using the Windows registry to obtain the font path by using DirectWrite. The goal is simply to improve the quality of the code. This doesn't change any functionality

[src\meson.build] Remove Uniscribe has dependency

Uniscribe was only used for the FontCollector. Since we now use DirectWrite, we don't need it anymore.

[src\dialog_fonts_collector] Catch exceptions that FontCollector may raise

On Windows, the initialization of the FontCollector can raise an exception

[src\font_file_lister] Document the exception that GdiFontFileLister can throw

[src\font_file_lister_gdi] Correct possible memory leak when an error occur

Fix error caused by AddFontResource on Windows 10 or higher

[meson.build] Replace add_project_arguments with conf.set for HAVE_DWRITE_3

[src\dialog_fonts_collector] Update message error and optimisation

[src\font_file_lister_gdi] Correct documentation typo

[src\font_file_lister_gdi] Cosmetic nit - Initialize hfont in one line

[src\font_file_lister_gdi] Cosmetic nit - Remove if statements brace

[src\font_file_lister_gdi] Replace WCHAR param of normalizeFilePathCase to std::wstring

[src\font_file_lister_gdi] Replace WCHAR by std::wstring

[src\font_file_lister_gdi] Use IDWriteFontFace::GetSimulations to detect fake_italic/fake_bold

See this comment: https://github.com/arch1t3cht/Aegisub/pull/107#issuecomment-1975229652

[src\font_file_lister_gdi] If Win7/8 has Win 10 SDK on compile time, correctly verify if font has character(s)

With the Visual Studio 2019 toolchain on Windows 7, it installs the Windows 10 SDK by default. Because of this, ``HAVE_DWRITE_3`` is true, so the ``QueryInterface`` always fails. Now, if the ``QueryInterface`` fails, we try to verify if the font has characters with a Windows Vista SP2 compatible code.

[src\font_file_lister_gdi] Support facename that contains only whitespace AND truncated facename

Problem 1:
Previously, if a user wrote "\fn   ", it would return the font Arial, which is not what we want. This is because when we request EnumFontFamiliesEx with whitespace or an empty lfFaceName, it will enumerate all the installed fonts.

Solution 1:
To resolve this issue, let's implement a solution similar to libass to determine if the selected facename exists: 649a7c2e1f/libass/ass_directwrite.c (L737-L747)

Problem 2:
GDI truncates font names to 31 characters. See: https://github.com/libass/libass/issues/459
However, since I changed the method to determine if a facename exists, I ensured that we still support this "feature".

To test this, I used the font in: https://github.com/libass/libass/issues/710

[src\font_file_lister_gdi] Add a FIXME comment regarding the utilization of std::wstring over WCHAR

[src\font_file_lister_gdi] Add FIXME comment about charset
This commit is contained in:
moi15moi 2024-01-31 20:48:34 -05:00 committed by arch1t3cht
parent 897ca32c26
commit 7543060f1d
5 changed files with 184 additions and 258 deletions

View File

@ -241,6 +241,10 @@ if host_machine.system() == 'windows' and not get_option('directsound').disabled
endif
endif
if host_machine.system() == 'windows' and cc.has_header('dwrite_3.h')
conf.set('HAVE_DWRITE_3', 1)
endif
if host_machine.system() == 'darwin'
frameworks_dep = dependency('appleframeworks', modules : ['CoreText', 'CoreFoundation', 'AppKit', 'Carbon', 'IOKit'])
deps += frameworks_dep

View File

@ -94,7 +94,14 @@ void FontsCollectorThread(AssFile *subs, agi::fs::path const& destination, FcMod
collector->AddPendingEvent(ValueEvent<color_str_pair>(EVT_ADD_TEXT, -1, {colour, text.Clone()}));
};
auto paths = FontCollector(AppendText).GetFontPaths(subs);
std::vector<agi::fs::path> paths;
try {
paths = FontCollector(AppendText).GetFontPaths(subs);
}
catch (agi::EnvironmentError const& err) {
AppendText(fmt_tl("* An error occurred when enumerating the used fonts: %s.\n", err.GetMessage()), 2);
}
if (paths.empty()) {
collector->AddPendingEvent(wxThreadEvent(EVT_COLLECTION_DONE));
return;

View File

@ -41,17 +41,17 @@ struct CollectionResult {
};
#ifdef _WIN32
#include <dwrite.h>
class GdiFontFileLister {
std::unordered_multimap<uint32_t, agi::fs::path> index;
agi::scoped_holder<HDC> dc;
std::string buffer;
bool ProcessLogFont(LOGFONTW const& expected, LOGFONTW const& actual, std::vector<int> const& characters);
agi::scoped_holder<HDC> dc_sh;
agi::scoped_holder<IDWriteFactory*> dwrite_factory_sh;
agi::scoped_holder<IDWriteFontCollection*> font_collection_sh;
agi::scoped_holder<IDWriteGdiInterop*> gdi_interop_sh;
public:
/// Constructor
/// @param cb Callback for status logging
GdiFontFileLister(FontCollectorStatusCallback &cb);
/// @throws agi::EnvironmentError if an error occurs during construction.
GdiFontFileLister(FontCollectorStatusCallback &);
/// @brief Get the path to the font with the given styles
/// @param facename Name of font face

View File

@ -16,284 +16,199 @@
#include "font_file_lister.h"
#include "compat.h"
#include <libaegisub/charset_conv_win.h>
#include <libaegisub/fs.h>
#include <libaegisub/io.h>
#include <libaegisub/log.h>
#include <ShlObj.h>
#include <boost/scope_exit.hpp>
#include <unicode/utf16.h>
#include <Usp10.h>
#include <dwrite.h>
#include <wchar.h>
#include <windowsx.h>
static void read_fonts_from_key(HKEY hkey, agi::fs::path font_dir, std::vector<agi::fs::path> &files) {
static const auto fonts_key_name = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
HKEY key;
auto ret = RegOpenKeyExW(hkey, fonts_key_name, 0, KEY_QUERY_VALUE, &key);
if (ret != ERROR_SUCCESS) return;
BOOST_SCOPE_EXIT_ALL(=) { RegCloseKey(key); };
#ifdef HAVE_DWRITE_3
#include <dwrite_3.h>
#endif
DWORD name_buf_size = SHRT_MAX;
DWORD data_buf_size = MAX_PATH;
/// @brief Normalize the case of a file path.
/// @param path The path to be normalized. It can be a directory or a file.
/// @return A string representing the normalized path.
/// If the path normalization fails due to file handling errors or other issues,
/// an empty string is returned.
/// @example For "C:\WINDOWS\FONTS\ARIAL.TTF", it would return "C:\Windows\Fonts\arial.ttf"
std::wstring normalizeFilePathCase(const std::wstring path) {
/* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */
HANDLE hfile = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (hfile == INVALID_HANDLE_VALUE)
return L"";
agi::scoped_holder<HANDLE> hfile_sh(hfile, [](HANDLE hfile) { CloseHandle(hfile); });
auto font_name = new wchar_t[name_buf_size];
auto font_filename = new wchar_t[data_buf_size];
DWORD normalized_path_length = GetFinalPathNameByHandle(hfile_sh, nullptr, 0, FILE_NAME_NORMALIZED);
if (!normalized_path_length)
return L"";
for (DWORD i = 0;; ++i) {
retry:
DWORD name_len = name_buf_size;
DWORD data_len = data_buf_size;
agi::scoped_holder<WCHAR*> normalized_path_sh(new WCHAR[normalized_path_length + 1], [](WCHAR* p) { delete[] p; });
if (!GetFinalPathNameByHandle(hfile_sh, normalized_path_sh, normalized_path_length + 1, FILE_NAME_NORMALIZED))
return L"";
ret = RegEnumValueW(key, i, font_name, &name_len, NULL, NULL, reinterpret_cast<BYTE*>(font_filename), &data_len);
if (ret == ERROR_MORE_DATA) {
data_buf_size = data_len;
delete font_filename;
font_filename = new wchar_t[data_buf_size];
goto retry;
}
if (ret == ERROR_NO_MORE_ITEMS) break;
if (ret != ERROR_SUCCESS) continue;
std::wstring normalized_path(normalized_path_sh);
agi::fs::path font_path(font_filename);
if (!agi::fs::FileExists(font_path))
// Doesn't make a ton of sense to do this with user fonts, but they seem to be stored as full paths anyway
font_path = font_dir / font_path;
if (agi::fs::FileExists(font_path)) // The path might simply be invalid
files.push_back(font_path);
}
// GetFinalPathNameByHandle return path into ``device path`` form. Ex: "\\?\C:\Windows\Fonts\ariali.ttf"
// We need to convert it to ``fully qualified DOS Path``. Ex: "C:\Windows\Fonts\ariali.ttf"
// There isn't any public API that remove the prefix (there is RtlNtPathNameToDosPathName, but it is really hacky to use it)
// See: https://stackoverflow.com/questions/31439011/getfinalpathnamebyhandle-result-without-prepended
// Even CPython remove the prefix manually: https://github.com/python/cpython/blob/963904335e579bfe39101adf3fd6a0cf705975ff/Lib/ntpath.py#L733-L793
// Gecko: https://github.com/mozilla/gecko-dev/blob/6032a565e3be7dcdd01e4fe26791c84f9222a2e0/widget/windows/WinUtils.cpp#L1577-L1584
if (normalized_path.compare(0, 7, L"\\\\?\\UNC") == 0)
normalized_path.erase(2, 6);
else if (normalized_path.compare(0, 4, L"\\\\?\\") == 0)
normalized_path.erase(0, 4);
delete font_name;
delete font_filename;
return normalized_path;
}
namespace {
uint32_t murmur3(const char *data, uint32_t len) {
static const uint32_t c1 = 0xcc9e2d51;
static const uint32_t c2 = 0x1b873593;
static const uint32_t r1 = 15;
static const uint32_t r2 = 13;
static const uint32_t m = 5;
static const uint32_t n = 0xe6546b64;
uint32_t hash = 0;
const int nblocks = len / 4;
auto blocks = reinterpret_cast<const uint32_t *>(data);
for (uint32_t i = 0; i * 4 < len; ++i) {
uint32_t k = blocks[i];
k *= c1;
k = _rotl(k, r1);
k *= c2;
hash ^= k;
hash = _rotl(hash, r2) * m + n;
}
hash ^= len;
hash ^= hash >> 16;
hash *= 0x85ebca6b;
hash ^= hash >> 13;
hash *= 0xc2b2ae35;
hash ^= hash >> 16;
return hash;
}
std::vector<agi::fs::path> get_installed_fonts() {
std::vector<agi::fs::path> files;
wchar_t fdir[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_FONTS, NULL, 0, fdir);
agi::fs::path font_dir(fdir);
// System fonts
read_fonts_from_key(HKEY_LOCAL_MACHINE, font_dir, files);
// User fonts
read_fonts_from_key(HKEY_CURRENT_USER, font_dir, files);
return files;
}
using font_index = std::unordered_multimap<uint32_t, agi::fs::path>;
font_index index_fonts(FontCollectorStatusCallback &cb) {
font_index hash_to_path;
auto fonts = get_installed_fonts();
std::unique_ptr<char[]> buffer(new char[1024]);
for (auto const& path : fonts) {
try {
auto stream = agi::io::Open(path, true);
stream->read(&buffer[0], 1024);
auto hash = murmur3(&buffer[0], stream->tellg());
hash_to_path.emplace(hash, path);
}
catch (agi::Exception const& e) {
cb(to_wx(e.GetMessage() + "\n"), 3);
}
}
return hash_to_path;
}
void get_font_data(std::string& buffer, HDC dc) {
buffer.clear();
// For ttc files we have to ask for the "ttcf" table to get the complete file
DWORD ttcf = 0x66637474;
auto size = GetFontData(dc, ttcf, 0, nullptr, 0);
if (size == GDI_ERROR) {
ttcf = 0;
size = GetFontData(dc, 0, 0, nullptr, 0);
}
if (size == GDI_ERROR || size == 0)
return;
buffer.resize(size);
GetFontData(dc, ttcf, 0, &buffer[0], size);
}
}
GdiFontFileLister::GdiFontFileLister(FontCollectorStatusCallback &cb)
: dc(CreateCompatibleDC(nullptr), [](HDC dc) { DeleteDC(dc); })
GdiFontFileLister::GdiFontFileLister(FontCollectorStatusCallback &)
: dwrite_factory_sh(nullptr, [](IDWriteFactory* p) { p->Release(); })
, font_collection_sh(nullptr, [](IDWriteFontCollection* p) { p->Release(); })
, dc_sh(nullptr, [](HDC dc) { DeleteDC(dc); })
, gdi_interop_sh(nullptr, [](IDWriteGdiInterop* p) { p->Release(); })
{
cb(_("Updating font cache\n"), 0);
index = index_fonts(cb);
IDWriteFactory* dwrite_factory;
if (FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast<IUnknown**>(&dwrite_factory))))
throw agi::EnvironmentError("Failed to initialize the DirectWrite Factory");
dwrite_factory_sh = dwrite_factory;
IDWriteFontCollection* font_collection;
if (FAILED(dwrite_factory_sh->GetSystemFontCollection(&font_collection, true)))
throw agi::EnvironmentError("Failed to initialize the system font collection");
font_collection_sh = font_collection;
HDC dc = CreateCompatibleDC(nullptr);
if (dc == nullptr)
throw agi::EnvironmentError("Failed to initialize the HDC");
dc_sh = dc;
IDWriteGdiInterop* gdi_interop;
if (FAILED(dwrite_factory_sh->GetGdiInterop(&gdi_interop)))
throw agi::EnvironmentError("Failed to initialize the Gdi Interop");
gdi_interop_sh = gdi_interop;
}
CollectionResult GdiFontFileLister::GetFontPaths(std::string const& facename, int bold, bool italic, std::vector<int> const& characters) {
CollectionResult ret;
int weight = bold == 0 ? 400 :
bold == 1 ? 700 :
bold;
// From VSFilter
// - https://sourceforge.net/p/guliverkli2/code/HEAD/tree/src/subtitles/RTS.cpp#l45
// - https://sourceforge.net/p/guliverkli2/code/HEAD/tree/src/subtitles/STS.cpp#l2992
LOGFONTW lf{};
lf.lfCharSet = DEFAULT_CHARSET;
wcsncpy(lf.lfFaceName, agi::charset::ConvertW(facename).c_str(), LF_FACESIZE);
lf.lfCharSet = DEFAULT_CHARSET; // FIXME: Note that this currently ignores the font encoding specified in the ass file.
wcsncpy_s(lf.lfFaceName, LF_FACESIZE, agi::charset::ConvertW(facename).c_str(), _TRUNCATE);
lf.lfItalic = italic ? -1 : 0;
lf.lfWeight = bold == 0 ? 400 :
bold == 1 ? 700 :
bold;
lf.lfWeight = weight;
lf.lfOutPrecision = OUT_TT_PRECIS;
lf.lfClipPrecision = CLIP_DEFAULT_PRECIS;
lf.lfQuality = ANTIALIASED_QUALITY;
lf.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
// Gather all of the styles for the given family name
std::vector<LOGFONTW> matches;
using type = decltype(matches);
EnumFontFamiliesEx(dc, &lf, [](const LOGFONT *lf, const TEXTMETRIC *, DWORD, LPARAM lParam) -> int {
reinterpret_cast<type*>(lParam)->push_back(*lf);
return 1;
}, (LPARAM)&matches, 0);
if (matches.empty())
agi::scoped_holder<HFONT> hfont_sh(CreateFontIndirect(&lf), [](HFONT p) { DeleteObject(p); });
if (hfont_sh == nullptr)
return ret;
// If the user asked for a non-regular style, verify that it actually exists
if (italic || bold) {
bool has_bold = false;
bool has_italic = false;
bool has_bold_italic = false;
SelectFont(dc_sh, hfont_sh);
auto is_italic = [&](LOGFONTW const& lf) {
return !italic || lf.lfItalic;
};
auto is_bold = [&](LOGFONTW const& lf) {
return !bold
|| (bold == 1 && lf.lfWeight >= 700)
|| (bold > 1 && lf.lfWeight > bold);
};
for (auto const& match : matches) {
has_bold = has_bold || is_bold(match);
has_italic = has_italic || is_italic(match);
has_bold_italic = has_bold_italic || (is_bold(match) && is_italic(match));
}
ret.fake_italic = !has_italic;
ret.fake_bold = (italic && has_italic ? !has_bold_italic : !has_bold);
}
// Use the family name supplied by EnumFontFamiliesEx as it may be a localized version
memcpy(lf.lfFaceName, matches[0].lfFaceName, LF_FACESIZE);
// Open the font and get the data for it to look up in the index
auto hfont = CreateFontIndirectW(&lf);
SelectObject(dc, hfont);
BOOST_SCOPE_EXIT_ALL(=) {
SelectObject(dc, nullptr);
DeleteObject(hfont);
};
get_font_data(buffer, dc);
auto range = index.equal_range(murmur3(buffer.c_str(), std::min<size_t>(buffer.size(), 1024U)));
if (range.first == range.second)
return ret; // could instead write to a temp dir
// Compare the full files for each of the fonts with the same prefix
std::unique_ptr<char[]> file_buffer(new char[buffer.size()]);
for (auto it = range.first; it != range.second; ++it) {
auto stream = agi::io::Open(it->second, true);
stream->read(&file_buffer[0], buffer.size());
if ((size_t)stream->tellg() != buffer.size())
continue;
if (memcmp(&file_buffer[0], &buffer[0], buffer.size()) == 0) {
ret.paths.push_back(it->second);
break;
}
}
// No fonts actually matched
if (ret.paths.empty())
std::wstring selected_name(LF_FACESIZE - 1, L'\0');
// FIXME: This will override the string's terminator, which is not technically correct.
// After switching to C++20 this should use .data().
if (!GetTextFaceW(dc_sh, LF_FACESIZE, &selected_name[0]))
return ret;
// Convert the characters to a utf-16 string
std::wstring utf16characters;
utf16characters.reserve(characters.size());
for (int chr : characters) {
if (U16_LENGTH(chr) == 1)
utf16characters.push_back(static_cast<wchar_t>(chr));
else {
utf16characters.push_back(U16_LEAD(chr));
utf16characters.push_back(U16_TRAIL(chr));
}
}
// If the selected_name is different then the lf.lfFaceName,
// it means that the requested font doesn't exist.
if (_wcsnicmp(&selected_name[0], lf.lfFaceName, LF_FACESIZE))
return ret;
SCRIPT_CACHE cache = nullptr;
std::unique_ptr<WORD[]> indices(new WORD[utf16characters.size()]);
IDWriteFontFace* font_face;
if (FAILED(gdi_interop_sh->CreateFontFaceFromHdc(dc_sh, &font_face)))
return ret;
agi::scoped_holder<IDWriteFontFace*> font_face_sh(font_face, [](IDWriteFontFace* p) { p->Release(); });
// First try to check glyph coverage with Uniscribe, since it
// handles non-BMP unicode characters
auto hr = ScriptGetCMap(dc, &cache, utf16characters.data(),
utf16characters.size(), 0, indices.get());
ret.fake_italic = font_face_sh->GetSimulations() & DWRITE_FONT_SIMULATIONS_OBLIQUE;
ret.fake_bold = font_face_sh->GetSimulations() & DWRITE_FONT_SIMULATIONS_BOLD;
// Uniscribe doesn't like some types of fonts, so fall back to GDI
if (hr == E_HANDLE) {
GetGlyphIndicesW(dc, utf16characters.data(), utf16characters.size(),
indices.get(), GGI_MARK_NONEXISTING_GLYPHS);
for (size_t i = 0; i < utf16characters.size(); ++i) {
if (U16_IS_SURROGATE(utf16characters[i]))
continue;
if (indices[i] == SHRT_MAX)
ret.missing += utf16characters[i];
}
}
else if (hr == S_FALSE) {
for (size_t i = 0; i < utf16characters.size(); ++i) {
// Uniscribe doesn't report glyph indexes for non-BMP characters,
// so we have to call ScriptGetCMap on each individual pair to
// determine if it's the missing one
if (U16_IS_LEAD(utf16characters[i])) {
hr = ScriptGetCMap(dc, &cache, &utf16characters[i], 2, 0, &indices[i]);
if (hr == S_FALSE) {
ret.missing += utf16characters[i];
ret.missing += utf16characters[i + 1];
}
++i;
}
else if (indices[i] == 0) {
ret.missing += utf16characters[i];
bool is_query_font_face_3_succeeded = false;
#ifdef HAVE_DWRITE_3
// Fonts added via the AddFontResource API are not included in the IDWriteFontCollection.
// This omission causes GetFontFromFontFace to fail.
// This issue is unavoidable on Windows 8 or lower.
// However, on Windows 10 or higher, we address this by querying IDWriteFontFace to IDWriteFontFace3.
// From this new instance, we can verify font character(s) availability.
IDWriteFontFace3* font_face_3;
if (SUCCEEDED(font_face_sh->QueryInterface(__uuidof(IDWriteFontFace3), (void**)&font_face_3))) {
agi::scoped_holder<IDWriteFontFace3*> font_face_3_sh(font_face_3, [](IDWriteFontFace3* p) { p->Release(); });
is_query_font_face_3_succeeded = true;
for (int character : characters) {
if (!font_face_3_sh->HasCharacter((UINT32)character)) {
ret.missing += character;
}
}
}
ScriptFreeCache(&cache);
#endif
if (!is_query_font_face_3_succeeded) {
IDWriteFont* font;
if (FAILED(font_collection_sh->GetFontFromFontFace(font_face_sh, &font)))
return ret;
agi::scoped_holder<IDWriteFont*> font_sh(font, [](IDWriteFont* p) { p->Release(); });
BOOL exists;
HRESULT hr;
for (int character : characters) {
hr = font_sh->HasCharacter((UINT32)character, &exists);
if (FAILED(hr) || !exists)
ret.missing += character;
}
}
UINT32 file_count = 1;
IDWriteFontFile* font_file;
// DirectWrite only supports one file per face
if (FAILED(font_face_sh->GetFiles(&file_count, &font_file)))
return ret;
agi::scoped_holder<IDWriteFontFile*> font_file_sh(font_file, [](IDWriteFontFile* p) { p->Release(); });
IDWriteFontFileLoader* loader;
if (FAILED(font_file_sh->GetLoader(&loader)))
return ret;
agi::scoped_holder<IDWriteFontFileLoader*> loader_sh(loader, [](IDWriteFontFileLoader* p) { p->Release(); });
IDWriteLocalFontFileLoader* local_loader;
if (FAILED(loader_sh->QueryInterface(__uuidof(IDWriteLocalFontFileLoader), (void**)&local_loader)))
return ret;
agi::scoped_holder<IDWriteLocalFontFileLoader*> local_loader_sh(local_loader, [](IDWriteLocalFontFileLoader* p) { p->Release(); });
LPCVOID font_file_reference_key;
UINT32 font_file_reference_key_size;
if (FAILED(font_file_sh->GetReferenceKey(&font_file_reference_key, &font_file_reference_key_size)))
return ret;
UINT32 path_length;
if (FAILED(local_loader_sh->GetFilePathLengthFromKey(font_file_reference_key, font_file_reference_key_size, &path_length)))
return ret;
std::wstring path(path_length, L'\0');
// FIXME: This will override the string's terminator, which is not technically correct.
// After switching to C++20 this should use .data().
if (FAILED(local_loader_sh->GetFilePathFromKey(font_file_reference_key, font_file_reference_key_size, &path[0], path_length + 1)))
return ret;
// DirectWrite always return the file path in upper case. Ex: "C:\WINDOWS\FONTS\ARIAL.TTF"
std::wstring normalized_path = normalizeFilePathCase(path);
if (normalized_path.empty())
return ret;
ret.paths.push_back(agi::fs::path(normalized_path));
return ret;
}

View File

@ -182,10 +182,10 @@ elif host_machine.system() == 'windows'
else
error('Missing Windows SDK GDI Library (wingdi.h / gdi32.lib)')
endif
if cc.has_header('usp10.h')
deps += cc.find_library('usp10', required: true)
if cc.has_header('dwrite.h')
deps += cc.find_library('dwrite', required: true)
else
error('Missing Windows SDK Uniscribe Library (usp10.h / usp10.lib)')
error('Missing Windows SDK DirectWrite Library (dwrite.h / dwrite.lib)')
endif
res_inc = include_directories('bitmaps/windows')