diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5ecc2622..80899060d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,10 @@ jobs: - name: Install dependencies (MacOS) if: matrix.config.os == 'macos-latest' run: | - brew update + export HOMEBREW_NO_INSTALL_CLEANUP=1 + export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 + # Skip brew update for now, see https://github.com/actions/setup-python/issues/577 + # brew update brew install luarocks ninja luarocks install luafilesystem 1.8.0 luarocks install moonscript --dev diff --git a/meson.build b/meson.build index 6ed11ef90..2dc1b4176 100644 --- a/meson.build +++ b/meson.build @@ -327,6 +327,10 @@ if host_machine.system() == 'windows' 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', 'QuartzCore']) deps += frameworks_dep diff --git a/src/ass_style.cpp b/src/ass_style.cpp index 0b778b328..ad3b9ccc9 100644 --- a/src/ass_style.cpp +++ b/src/ass_style.cpp @@ -192,6 +192,7 @@ void AssStyle::UpdateData() { void AssStyle::GetEncodings(wxArrayString &encodingStrings) { encodingStrings.Clear(); + encodingStrings.Add(wxString("-1 - ") + _("Auto-detect base direction (libass only)")); encodingStrings.Add(wxString("0 - ") + _("ANSI")); encodingStrings.Add(wxString("1 - ") + _("Default")); encodingStrings.Add(wxString("2 - ") + _("Symbol")); diff --git a/src/dialog_fonts_collector.cpp b/src/dialog_fonts_collector.cpp index bc97c152d..e29526334 100644 --- a/src/dialog_fonts_collector.cpp +++ b/src/dialog_fonts_collector.cpp @@ -94,7 +94,14 @@ void FontsCollectorThread(AssFile *subs, agi::fs::path const& destination, FcMod collector->AddPendingEvent(ValueEvent(EVT_ADD_TEXT, -1, {colour, text.Clone()})); }; - auto paths = FontCollector(AppendText).GetFontPaths(subs); + std::vector 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; diff --git a/src/dialog_style_editor.cpp b/src/dialog_style_editor.cpp index 2b511c282..d48047c71 100644 --- a/src/dialog_style_editor.cpp +++ b/src/dialog_style_editor.cpp @@ -258,7 +258,7 @@ DialogStyleEditor::DialogStyleEditor(wxWindow *parent, AssStyle *style, agi::Con break; } } - if (!found) Encoding->Select(0); + if (!found) Encoding->Select(2); // Style name sizer NameSizer->Add(StyleName, 1, wxALL, 0); @@ -474,8 +474,10 @@ void DialogStyleEditor::UpdateWorkStyle() { work->font = from_wx(FontName->GetValue()); + wxString encoding_selection = Encoding->GetValue(); + wxString encoding_num = encoding_selection.substr(0, 1) + encoding_selection.substr(1).BeforeFirst('-'); // Have to account for -1 long templ = 0; - Encoding->GetValue().BeforeFirst('-').ToLong(&templ); + encoding_num.ToLong(&templ); work->encoding = templ; work->borderstyle = OutlineType->IsChecked() ? 3 : 1; diff --git a/src/font_file_lister.h b/src/font_file_lister.h index 82f02cca9..171d61958 100644 --- a/src/font_file_lister.h +++ b/src/font_file_lister.h @@ -41,17 +41,17 @@ struct CollectionResult { }; #ifdef _WIN32 +#include class GdiFontFileLister { - std::unordered_multimap index; - agi::scoped_holder dc; - std::string buffer; - - bool ProcessLogFont(LOGFONTW const& expected, LOGFONTW const& actual, std::vector const& characters); + agi::scoped_holder dc_sh; + agi::scoped_holder dwrite_factory_sh; + agi::scoped_holder font_collection_sh; + agi::scoped_holder 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 diff --git a/src/font_file_lister_gdi.cpp b/src/font_file_lister_gdi.cpp index 14ff9261a..0d625f590 100644 --- a/src/font_file_lister_gdi.cpp +++ b/src/font_file_lister_gdi.cpp @@ -16,284 +16,199 @@ #include "font_file_lister.h" -#include "compat.h" - #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include -static void read_fonts_from_key(HKEY hkey, agi::fs::path font_dir, std::vector &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 +#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 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 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(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(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 get_installed_fonts() { - std::vector 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; - -font_index index_fonts(FontCollectorStatusCallback &cb) { - font_index hash_to_path; - auto fonts = get_installed_fonts(); - std::unique_ptr 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(&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 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 matches; - using type = decltype(matches); - EnumFontFamiliesEx(dc, &lf, [](const LOGFONT *lf, const TEXTMETRIC *, DWORD, LPARAM lParam) -> int { - reinterpret_cast(lParam)->push_back(*lf); - return 1; - }, (LPARAM)&matches, 0); - - if (matches.empty()) + agi::scoped_holder 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(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 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(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 indices(new WORD[utf16characters.size()]); + IDWriteFontFace* font_face; + if (FAILED(gdi_interop_sh->CreateFontFaceFromHdc(dc_sh, &font_face))) + return ret; + agi::scoped_holder 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 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 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 font_file_sh(font_file, [](IDWriteFontFile* p) { p->Release(); }); + + IDWriteFontFileLoader* loader; + if (FAILED(font_file_sh->GetLoader(&loader))) + return ret; + agi::scoped_holder 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 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; } diff --git a/src/meson.build b/src/meson.build index bd67c1412..a9ad84501 100644 --- a/src/meson.build +++ b/src/meson.build @@ -187,10 +187,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')