From a89e3954acf4d074471afdd874fbd28b2ce4f682 Mon Sep 17 00:00:00 2001 From: Mohammad Al Tahan Date: Thu, 10 Mar 2022 09:38:28 +0200 Subject: [PATCH] Add support for bidirectional text using fribidi --- .cirrus.yml | 2 +- README.md | 5 +- cmake/FindFribidi.cmake | 45 +++++ docker/fedora-latest-m32/Dockerfile | 2 +- get-dependencies_linux.sh | 12 +- lib/ivis_opengl/CMakeLists.txt | 7 +- lib/ivis_opengl/textdraw.cpp | 292 ++++++++++++++++++++++------ src/seqdisp.cpp | 2 +- vcpkg.json | 1 + 9 files changed, 298 insertions(+), 70 deletions(-) create mode 100644 cmake/FindFribidi.cmake diff --git a/.cirrus.yml b/.cirrus.yml index cf91f799cfa..360463fb39b 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -15,7 +15,7 @@ freebsd_build_task: install_script: - pkg update -f - pkg clean -a -y - - pkg install -y git cmake ninja p7zip gettext pkgconf png sdl2 openal-soft physfs libvorbis libogg opus libtheora freetype2 harfbuzz curl libsodium sqlite3 rubygem-asciidoctor + - pkg install -y git cmake ninja p7zip gettext pkgconf png sdl2 openal-soft physfs libvorbis libogg opus libtheora freetype2 fribidi harfbuzz curl libsodium sqlite3 rubygem-asciidoctor init_git_submodules_script: git submodule update --init --recursive diff --git a/README.md b/README.md index 0a498dd784d..c5d2eff7912 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,7 @@ Do **not** use GitHub's "Download Zip" option, as it does not contain submodules * [opus](https://github.com/xiph/opus) * [Freetype](https://www.freetype.org/) * [Harfbuzz](https://github.com/harfbuzz/harfbuzz) ≥ 1.0 + * [fribidi] (https://github.com/fribidi/fribidi) * [OpenAL-Soft](https://openal-soft.org) * [libcurl](https://curl.haxx.se/libcurl/) _(strongly recommended: ≥ 7.58.0)_ * [libsodium](https://github.com/jedisct1/libsodium) ≥ 1.0.14 @@ -336,13 +337,13 @@ Do **not** use GitHub's "Download Zip" option, as it does not contain submodules ```shell sudo apt-get -u update sudo apt-get -y install git gcc g++ clang cmake libc-dev dpkg-dev ninja-build zip unzip pkg-config gettext asciidoctor - sudo apt-get -y install libpng-dev libsdl2-dev libopenal-dev libphysfs-dev libvorbis-dev libtheora-dev libxrandr-dev libfribidi-dev libfreetype6-dev libharfbuzz-dev libfontconfig1-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev + sudo apt-get -y install libpng-dev libsdl2-dev libopenal-dev libphysfs-dev libvorbis-dev libtheora-dev libxrandr-dev libfribidi-dev libfreetype6-dev libfribidi-dev libharfbuzz-dev libfontconfig1-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev ``` * Manually (Fedora): ```shell sudo dnf -y update && dnf clean all sudo dnf -y install git gcc gcc-c++ cmake ninja-build p7zip gettext rubygem-asciidoctor - sudo dnf -y install libpng-devel SDL2-devel openal-soft-devel physfs-devel libogg-devel libvorbis-devel libtheora-devel freetype-devel harfbuzz-devel libcurl-devel openssl-devel libsodium-devel sqlite-devel + sudo dnf -y install libpng-devel SDL2-devel openal-soft-devel physfs-devel libogg-devel libvorbis-devel libtheora-devel freetype-devel fribidi harfbuzz-devel libcurl-devel openssl-devel libsodium-devel sqlite-devel ``` * **Building from the command-line:** 1. Starting from the _parent_ directory of the warzone2100 source code (which is assumed to be in a folder named `warzone2100`), create a **sibling** build directory: diff --git a/cmake/FindFribidi.cmake b/cmake/FindFribidi.cmake new file mode 100644 index 00000000000..558ea2ccb66 --- /dev/null +++ b/cmake/FindFribidi.cmake @@ -0,0 +1,45 @@ +# Locate fribidi +# This module defines: +# +# FRIBIDI_INCLUDE_DIRS +# FRIBIDI_LIBRARIS +# FRIBIDI_FOUND +# FRIBIDI_VERSION_STRING +# + +find_package(PkgConfig) +# If PkgConfig is available, attempt to find Fribidi paths using PkgConfig +if(PkgConfig_FOUND) + if(Fribidi_FIND_VERSION) + if(Fribidi_FIND_VERSION_EXACT) + set(_pkgConfig_version_match "==${Fribidi_FIND_VERSION}") + else() + set(_pkgConfig_version_match ">=${Fribidi_FIND_VERSION}") + endif() + endif() + + pkg_search_module(PCFG_FRIBIDI QUIET fribidi${_pkgConfig_version_match}) + + if(PCFG_FRIBIDI_FOUND) + # Found Fribidi with PkgConfig + message(STATUS "Detected Fribidi with PkgConfig: FRIBIDI_INCLUDE_DIRS (${PCFG_FRIBIDI_INCLUDE_DIRS}); FRIBIDI_LIBRARY_DIRS (${PCFG_FRIBIDI_LIBRARY_DIRS})") + endif() +endif() + +# Attempt to find Fribidi + +find_path( + FRIBIDI_INCLUDE_DIRS fribidi.h + HINTS ${PCFG_FRIBIDI_INCLUDE_DIRS} ${PCFG_FRIBIDI_INCLUDEDIR} +) + +find_library( + FRIBIDI_LIBRARIES NAMES fribidi + HINTS ${PCFG_FRIBIDI_LIBRARY_DIRS} ${PCFG_FRIBIDI_LIBDIR} +) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(Fribidi REQUIRED_VARS FRIBIDI_INCLUDE_DIRS FRIBIDI_LIBRARIES VERSION_VAR FRIBIDI_VERSION_STRING) + +mark_as_advanced(FRIBIDI_INCLUDE_DIRS FRIBIDI_LIBRARIES) diff --git a/docker/fedora-latest-m32/Dockerfile b/docker/fedora-latest-m32/Dockerfile index fff60023136..655096d5d94 100644 --- a/docker/fedora-latest-m32/Dockerfile +++ b/docker/fedora-latest-m32/Dockerfile @@ -8,7 +8,7 @@ RUN dnf -y update && dnf -y install git gcc gcc-c++ cmake ninja-build p7zip gett # Install WZ dependencies - 32-bit (i686, for cross-compile) RUN dnf -y update && dnf -y install glibc-devel.i686 \ - && dnf -y install libpng-devel.i686 SDL2-devel.i686 openal-soft-devel.i686 physfs-devel.i686 libogg-devel.i686 libvorbis-devel.i686 opus-devel.i686 libtheora-devel.i686 freetype-devel.i686 harfbuzz-devel.i686 libcurl-devel.i686 openssl-devel.i686 libsodium-devel.i686 sqlite-devel.i686 \ + && dnf -y install libpng-devel.i686 SDL2-devel.i686 openal-soft-devel.i686 physfs-devel.i686 libogg-devel.i686 libvorbis-devel.i686 opus-devel.i686 libtheora-devel.i686 fribidi-devel.i686 freetype-devel.i686 harfbuzz-devel.i686 libcurl-devel.i686 openssl-devel.i686 libsodium-devel.i686 sqlite-devel.i686 \ && dnf clean all # Defines arguments which can be passed during build time. diff --git a/get-dependencies_linux.sh b/get-dependencies_linux.sh index b3c49d97e24..217c92df64a 100755 --- a/get-dependencies_linux.sh +++ b/get-dependencies_linux.sh @@ -52,10 +52,10 @@ if [ "${DISTRO}" == "ubuntu" ]; then if [ "${VERSION_PARTS[0]}" -eq "18" ]; then echo "Installing build-dependencies for Ubuntu 18.x" - DEBIAN_FRONTEND=noninteractive apt-get -y install cmake git zip unzip gettext asciidoctor libsdl2-dev libphysfs-dev libpng-dev libopenal-dev libvorbis-dev libogg-dev libopus-dev libtheora-dev libxrandr-dev libfreetype6-dev libharfbuzz-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev + DEBIAN_FRONTEND=noninteractive apt-get -y install cmake git zip unzip gettext asciidoctor libsdl2-dev libphysfs-dev libpng-dev libopenal-dev libvorbis-dev libogg-dev libopus-dev libtheora-dev libxrandr-dev libfreetype6-dev libfribidi-dev libharfbuzz-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev elif [ "${VERSION_PARTS[0]}" -ge "20" ]; then echo "Installing build-dependencies for Ubuntu 20.x+" - DEBIAN_FRONTEND=noninteractive apt-get -y install cmake git zip unzip gettext asciidoctor libsdl2-dev libphysfs-dev libpng-dev libopenal-dev libvorbis-dev libogg-dev libopus-dev libtheora-dev libxrandr-dev libfreetype-dev libharfbuzz-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev + DEBIAN_FRONTEND=noninteractive apt-get -y install cmake git zip unzip gettext asciidoctor libsdl2-dev libphysfs-dev libpng-dev libopenal-dev libvorbis-dev libogg-dev libopus-dev libtheora-dev libxrandr-dev libfreetype-dev libfribidi-dev libharfbuzz-dev libcurl4-gnutls-dev gnutls-dev libsodium-dev libsqlite3-dev else echo "Script does not currently support Ubuntu ${VERSION_PARTS[0]} (${VERSION})" exit 1 @@ -78,7 +78,7 @@ if [ "${DISTRO}" == "fedora" ]; then fi echo "Installing build-dependencies for Fedora" - dnf -y install cmake git p7zip gettext rubygem-asciidoctor SDL2-devel physfs-devel libpng-devel openal-soft-devel libvorbis-devel libogg-devel opus-devel libtheora-devel freetype-devel harfbuzz-devel libcurl-devel openssl-devel libsodium-devel sqlite-devel + dnf -y install cmake git p7zip gettext rubygem-asciidoctor SDL2-devel physfs-devel libpng-devel openal-soft-devel libvorbis-devel libogg-devel opus-devel libtheora-devel freetype-devel fribidi-devel harfbuzz-devel libcurl-devel openssl-devel libsodium-devel sqlite-devel dnf -y install vulkan-devel glslc fi @@ -95,7 +95,7 @@ if [ "${DISTRO}" == "alpine" ]; then fi echo "Installing build-dependencies for Alpine" - apk add --no-cache cmake git p7zip gettext asciidoctor sdl2-dev physfs-dev libpng-dev openal-soft-dev libvorbis-dev libogg-dev opus-dev libtheora-dev freetype-dev harfbuzz-dev curl-dev libsodium-dev sqlite-dev + apk add --no-cache cmake git p7zip gettext asciidoctor sdl2-dev physfs-dev libpng-dev openal-soft-dev libvorbis-dev libogg-dev opus-dev libtheora-dev freetype-dev fribidi-dev harfbuzz-dev curl-dev libsodium-dev sqlite-dev fi ################## @@ -111,7 +111,7 @@ if [ "${DISTRO}" == "archlinux" ]; then fi echo "Installing build-dependencies for ArchLinux" - pacman -S --noconfirm cmake git p7zip gettext asciidoctor sdl2 physfs libpng openal libvorbis libogg opus libtheora xorg-xrandr freetype2 harfbuzz curl libsodium sqlite + pacman -S --noconfirm cmake git p7zip gettext asciidoctor sdl2 physfs libpng openal libvorbis libogg opus libtheora xorg-xrandr freetype2 fribidi harfbuzz curl libsodium sqlite fi ################## @@ -127,7 +127,7 @@ if [ "${DISTRO}" == "opensuse-tumbleweed" ]; then fi echo "Installing build-dependencies for OpenSUSE Tumbleweed" - zypper install -y libSDL2-devel libphysfs-devel libpng16-devel libtheora-devel libvorbis-devel libogg-devel libopus-devel freetype-devel harfbuzz-devel openal-soft-devel libsodium-devel sqlite3-devel libtinygettext0 ruby3.0-rubygem-asciidoctor vulkan-devel + zypper install -y libSDL2-devel libphysfs-devel libpng16-devel libtheora-devel libvorbis-devel libogg-devel libopus-devel freetype-devel fribidi-devel harfbuzz-devel openal-soft-devel libsodium-devel sqlite3-devel libtinygettext0 ruby3.0-rubygem-asciidoctor vulkan-devel fi ################## diff --git a/lib/ivis_opengl/CMakeLists.txt b/lib/ivis_opengl/CMakeLists.txt index 014d5a27242..640da1444f5 100644 --- a/lib/ivis_opengl/CMakeLists.txt +++ b/lib/ivis_opengl/CMakeLists.txt @@ -50,6 +50,7 @@ file(GLOB SRC find_package(PNG 1.2 REQUIRED) find_package(Freetype REQUIRED) find_package(Harfbuzz 1.0 REQUIRED) +find_package(Fribidi REQUIRED) include(CheckCXXCompilerFlag) @@ -57,10 +58,12 @@ add_library(ivis-opengl STATIC ${HEADERS} ${SRC}) set_property(TARGET ivis-opengl PROPERTY FOLDER "lib") include(WZTargetConfiguration) WZ_TARGET_CONFIGURATION(ivis-opengl) -target_include_directories(ivis-opengl PRIVATE ${HARFBUZZ_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIR_ft2build}) -target_link_libraries(ivis-opengl PRIVATE framework PNG::PNG ${HARFBUZZ_LIBRARIES} ${FREETYPE_LIBRARIES}) + +target_include_directories(ivis-opengl PRIVATE ${HARFBUZZ_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIR_ft2build} ${FRIBIDI_INCLUDE_DIRS}) +target_link_libraries(ivis-opengl PRIVATE framework PNG::PNG ${HARFBUZZ_LIBRARIES} ${FREETYPE_LIBRARIES} ${FRIBIDI_LIBRARIES}) target_link_libraries(ivis-opengl PUBLIC glad) target_link_libraries(ivis-opengl PUBLIC optional-lite) + if(WZ_ENABLE_BACKEND_VULKAN) find_package(VulkanHeaders 148) # minimum supported version of the Vulkan headers is 148 set(_vulkanheaders_dl_gittag "sdk-1.2.198.0") diff --git a/lib/ivis_opengl/textdraw.cpp b/lib/ivis_opengl/textdraw.cpp index 74185332a5a..8a2eb42d272 100644 --- a/lib/ivis_opengl/textdraw.cpp +++ b/lib/ivis_opengl/textdraw.cpp @@ -44,6 +44,8 @@ // Contains the font color in the following order: red, green, blue, alpha static float font_colour[4] = {1.f, 1.f, 1.f, 1.f}; +#include "fribidi.h" +#include "3rdparty/utfcpp/source/utf8.h" #include "hb.h" #include "hb-ft.h" #include "ft2build.h" @@ -239,8 +241,19 @@ struct TextRun { std::string text; std::string language; + + int startOffset; + int endOffset; hb_script_t script; hb_direction_t direction; + hb_buffer_t* buffer; + unsigned int glyphCount; + hb_glyph_info_t* glyphInfos; + hb_glyph_position_t* glyphPositions; + + uint32_t* codePoints; + + TextRun() : startOffset(0), endOffset(0) {} TextRun(const std::string &t, const std::string &l, hb_script_t s, hb_direction_t d) : text(t), language(l), script(s), direction(d) {} @@ -289,6 +302,23 @@ struct DrawTextResult // only minimal visual difference. struct TextShaper { + hb_buffer_t* m_buffer; + + struct HarfbuzzPosition + { + hb_codepoint_t codepoint; + Vector2i penPosition; + + HarfbuzzPosition(hb_codepoint_t c, Vector2i &&p) : codepoint(c), penPosition(p) {} + }; + + struct ShapingResult + { + std::vector glyphes; + int32_t x_advance = 0; + int32_t y_advance = 0; + }; + TextShaper() { m_buffer = hb_buffer_create(); @@ -300,9 +330,10 @@ struct TextShaper } // Returns the text width and height *IN PIXELS* - TextLayoutMetrics getTextMetrics(const TextRun& text, FTFace &face) + TextLayoutMetrics getTextMetrics(const std::string& text, FTFace &face) { - const ShapingResult &shapingResult = shapeText(text, face); + const ShapingResult& shapingResult = shapeText(text, face); + if (shapingResult.glyphes.empty()) { return TextLayoutMetrics(shapingResult.x_advance / 64, shapingResult.y_advance / 64); @@ -335,10 +366,25 @@ struct TextShaper return TextLayoutMetrics(std::max(texture_width, x_advance), std::max(texture_height, y_advance)); } + FriBidiBracketType getBaseDirection() + { + std::string language = getLanguage(); + + if (language == "ar_SA") + { + return HB_DIRECTION_RTL; + } + else + { + return HB_DIRECTION_LTR; + } + } + // Draws the text and returns the text buffer, width and height, etc *IN PIXELS* - DrawTextResult drawText(const TextRun& text, FTFace &face) + DrawTextResult drawText(const std::string& text, FTFace &face) { - const ShapingResult &shapingResult = shapeText(text, face); + ShapingResult shapingResult = shapeText(text, face); + if (shapingResult.glyphes.empty()) { return DrawTextResult(RenderedText(), TextLayoutMetrics(shapingResult.x_advance / 64, shapingResult.y_advance / 64)); @@ -380,9 +426,11 @@ struct TextShaper const uint32_t y_advance = (shapingResult.y_advance / 64); const size_t stringTextureSize = 4 * texture_width * texture_height; + std::unique_ptr stringTexture(new unsigned char[stringTextureSize]); memset(stringTexture.get(), 0, stringTextureSize); + // TODO: Someone should document this piece. size_t glyphNum = 0; std::for_each(glyphs.begin(), glyphs.end(), [&](const glyphRaster &g) @@ -395,10 +443,10 @@ struct TextShaper { uint32_t j0 = g.pixelPosition.x - min_x; const auto srcBufferPos = i * g.pitch + 3 * j; - ASSERT(srcBufferPos + 2 < glyphBufferSize, "Invalid source (%" PRIu32" / %" PRIu32") reading glyph %zu for string \"%s\"; (%d, %d, %d, %d, %" PRIu32 ", %d, %d, %d, %" PRIu32 ", %" PRIu32 ")", srcBufferPos, glyphBufferSize, glyphNum, text.text.c_str(), i, g.size.y, g.pixelPosition.y, min_y, i0, j, g.pixelPosition.x, min_x, j0, g.pitch); + ASSERT(srcBufferPos + 2 < glyphBufferSize, "Invalid source (%" PRIu32" / %" PRIu32") reading glyph %zu for string \"%s\"; (%d, %d, %d, %d, %" PRIu32 ", %d, %d, %d, %" PRIu32 ", %" PRIu32 ")", srcBufferPos, glyphBufferSize, glyphNum, text.c_str(), i, g.size.y, g.pixelPosition.y, min_y, i0, j, g.pixelPosition.x, min_x, j0, g.pitch); uint8_t const *src = &g.buffer[srcBufferPos]; const auto stringTexturePos = 4 * ((i0 + i) * texture_width + j + j0); - ASSERT(stringTexturePos + 3 < stringTextureSize, "Invalid destination (%" PRIu32" / %zu) writing glyph %zu for string \"%s\"; (%d, %d, %d, %d, %" PRIu32 ", %d, %d, %d, %" PRIu32 ", %" PRIu32 ")", stringTexturePos, stringTextureSize, glyphNum, text.text.c_str(), i, g.size.y, g.pixelPosition.y, min_y, i0, j, g.pixelPosition.x, min_x, j0, texture_width); + ASSERT(stringTexturePos + 3 < stringTextureSize, "Invalid destination (%" PRIu32" / %zu) writing glyph %zu for string \"%s\"; (%d, %d, %d, %d, %" PRIu32 ", %d, %d, %d, %" PRIu32 ", %" PRIu32 ")", stringTexturePos, stringTextureSize, glyphNum, text.c_str(), i, g.size.y, g.pixelPosition.y, min_y, i0, j, g.pixelPosition.x, min_x, j0, texture_width); uint8_t *dst = &stringTexture[stringTexturePos]; dst[0] = std::min(dst[0] + src[0], 255); dst[1] = std::min(dst[1] + src[1], 255); @@ -408,66 +456,198 @@ struct TextShaper } ++glyphNum; }); + return DrawTextResult( RenderedText(std::move(stringTexture), texture_width, texture_height, min_x, min_y), TextLayoutMetrics(std::max(texture_width, x_advance), std::max(texture_height, y_advance)) ); } -public: - hb_buffer_t* m_buffer; - - struct HarfbuzzPosition + ShapingResult shapeText(const std::string& text, FTFace &face) { - hb_codepoint_t codepoint; - Vector2i penPosition; + /* Fribidi assumes that the text is encoded in UTF-32, so we have to + convert from UTF-8 to UTF-32, assuming that the string is indeed in UTF-8.*/ + std::u32string u32 = utf8::utf8to32(text); + + // Step 1: Initialize fribidi variables. + // TODO: Don't forget to delete them at the end. + + hb_script_t* scripts; + FriBidiCharType* types; + FriBidiLevel* levels; + FriBidiBracketType* bracketedTypes; + uint32_t* codePoints; + + FriBidiParType baseDirection; + int size; + + baseDirection = getBaseDirection(); + size = u32.length(); + + codePoints = new uint32_t[size]; + memset(codePoints, 0, size * sizeof(*codePoints)); + memcpy(codePoints, u32.c_str(), size * sizeof(*codePoints)); + scripts = new hb_script_t[size]; + memset(scripts, 0, size * sizeof(*scripts)); + types = new FriBidiCharType[size]; + memset(types, 0, size * sizeof(*types)); + levels = new FriBidiLevel[size]; + memset(levels, 0, size * sizeof(*levels)); + bracketedTypes = new FriBidiBracketType[size]; + memset(bracketedTypes, 0, size * sizeof(*bracketedTypes)); + + + // Step 2: Run fribidi. + + /* Get the bidi type of each character in the string.*/ + fribidi_get_bidi_types(codePoints, size, types); + fribidi_get_bracket_types(codePoints, size, types, bracketedTypes); + +#if defined(FRIBIDI_MAJOR_VERSION) && FRIBIDI_MAJOR_VERSION >= 1 + FriBidiLevel maxLevel = fribidi_get_par_embedding_levels_ex(types, bracketedTypes, size, &baseDirection, levels); + ASSERT(maxLevel != 0, "Error in fribidi_get_par_embedding_levels_ex!"); +#else + FriBidiLevel maxLevel = fribidi_get_par_embedding_levels(types, size, &baseDirection, levels); + ASSERT(maxLevel != 0, "Error in fribidi_get_par_embedding_levels_ex!"); +#endif + + /* Fill the array of scripts with scripts of each character */ + hb_unicode_funcs_t* funcs = hb_unicode_funcs_get_default(); + for (int i = 0; i < size; ++i) + scripts[i] = hb_unicode_script(funcs, codePoints[i]); - HarfbuzzPosition(hb_codepoint_t c, Vector2i &&p) : codepoint(c), penPosition(p) {} - }; - struct ShapingResult - { - std::vector glyphes; - int32_t x_advance = 0; - int32_t y_advance = 0; - }; + // Step 3: Resolve common or inherited scripts. - ShapingResult shapeText(const TextRun& text, FTFace &face) - { - hb_buffer_reset(m_buffer); - size_t length = std::min(text.text.size(), static_cast(std::numeric_limits::max())); - int textLength = static_cast(length); + hb_script_t lastScriptValue; + int lastScriptIndex = -1; + int lastSetIndex = -1; - hb_buffer_add_utf8(m_buffer, text.text.c_str(), textLength, 0, textLength); - hb_buffer_guess_segment_properties(m_buffer); - hb_buffer_set_flags(m_buffer, (hb_buffer_flags_t)(HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT)); + for (int i = 0; i < size; ++i) + { + if (scripts[i] == HB_SCRIPT_COMMON || scripts[i] == HB_SCRIPT_INHERITED) + { + if (lastScriptIndex != -1) + { + scripts[i] = lastScriptValue; + lastSetIndex = i; + } + } + else + { + for (int j = lastSetIndex + 1; j < i; ++j) + { + scripts[j] = scripts[i]; + } + lastScriptValue = scripts[i]; + lastScriptIndex = i; + lastSetIndex = i; + } + } - // harfbuzz shaping - std::array features = { {HBFeature::KerningOn, HBFeature::LigatureOn, HBFeature::CligOn} }; - hb_shape(face.m_font, m_buffer, features.data(), static_cast(features.size())); - unsigned int glyphCount; - hb_glyph_info_t *glyphInfo = hb_buffer_get_glyph_infos(m_buffer, &glyphCount); - hb_glyph_position_t *glyphPos = hb_buffer_get_glyph_positions(m_buffer, &glyphCount); - if (glyphCount == 0) + // Step 4: Create the different runs + + std::vector textRuns; + + hb_script_t lastScript = scripts[0]; + int lastLevel = levels[0]; + int lastRunStart = 0; // where the last run started + + /* i == size means that we've reached the end of the string, + and that the last run should be created.*/ + for (int i = 0; i <= size; ++i) + { + /* If the script or level is of the current point is the same as the previous one, + then this means that the we have not reached the end of the current run. + If there's change, create a new run.*/ + if (i == size || (scripts[i] != lastScript) || (levels[i] != lastLevel)) + { + TextRun run; + run.startOffset = lastRunStart; + run.endOffset = i; + run.script = lastScript; + run.codePoints = codePoints; + + /* "lastLevel & 1" yields either 1 or 0, depending on the least significant bit of lastLevel.*/ + run.direction = lastLevel & 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR; + + textRuns.push_back(run); + + if (i < size) + { + lastScript = scripts[i]; + lastLevel = levels[i]; + lastRunStart = i; + } + else + { + break; + } + } + } + + + // Step 6: Shape each run using harfbuzz. + + ShapingResult shapingResult; + + for (int i = 0; i < textRuns.size(); ++i) { - return {}; + shapeHarfbuzz(textRuns[i], face); } int32_t x = 0; int32_t y = 0; - ShapingResult result; - for (unsigned int glyphIndex = 0; glyphIndex < glyphCount; ++glyphIndex) + + /* Theoretically, the direction of loop must change depending on the base direction + (the current direction assumes that the text is RTL). However, since English and + other European strings does not include Arabic or Hebrew words, this direction + will be all that is needed.*/ + for (int i = (textRuns.size() - 1); i >= 0; --i) { - hb_glyph_position_t ¤t_glyphPos = glyphPos[glyphIndex]; - result.glyphes.emplace_back(glyphInfo[glyphIndex].codepoint, Vector2i(x + current_glyphPos.x_offset, y + current_glyphPos.y_offset)); + TextRun run = textRuns[i]; - x += glyphPos[glyphIndex].x_advance; - y += glyphPos[glyphIndex].y_advance; - }; - result.x_advance = x; - result.y_advance = y; - return result; + for (unsigned int glyphIndex = 0; glyphIndex < run.glyphCount; ++glyphIndex) + { + hb_glyph_position_t& current_glyphPos = run.glyphPositions[glyphIndex]; + + shapingResult.glyphes.emplace_back(run.glyphInfos[glyphIndex].codepoint, Vector2i(x + current_glyphPos.x_offset, y + current_glyphPos.y_offset)); + + x += run.glyphPositions[glyphIndex].x_advance; + y += run.glyphPositions[glyphIndex].y_advance; + } + } + shapingResult.x_advance += x; + shapingResult.y_advance += y; + + + // Step 7: Finalize. + + delete[] scripts; + delete[] types; + delete[] levels; + delete[] bracketedTypes; + delete[] codePoints; + + return shapingResult; + } + + void shapeHarfbuzz(TextRun& run, FTFace& face) + { + run.buffer = hb_buffer_create(); + hb_buffer_set_direction(run.buffer, run.direction); + hb_buffer_set_script(run.buffer, run.script); + hb_buffer_add_utf32(run.buffer, run.codePoints + run.startOffset, + run.endOffset - run.startOffset, 0, + run.endOffset - run.startOffset); + hb_buffer_set_flags(run.buffer, (hb_buffer_flags_t)(HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT)); + std::array features = { {HBFeature::KerningOn, HBFeature::LigatureOn, HBFeature::CligOn} }; + + hb_shape(face.m_font, run.buffer, features.data(), static_cast(features.size())); + + run.glyphInfos = hb_buffer_get_glyph_infos(run.buffer, &run.glyphCount); + run.glyphPositions = hb_buffer_get_glyph_positions(run.buffer, &run.glyphCount); } }; @@ -621,10 +801,9 @@ unsigned int height_pixelsToPoints(unsigned int heightInPixels) } // Returns the text width *in points* -unsigned int iV_GetTextWidth(const char *string, iV_fonts fontID) +unsigned int iV_GetTextWidth(const char* string, iV_fonts fontID) { - TextRun tr(string, "en", HB_SCRIPT_COMMON, HB_DIRECTION_LTR); - TextLayoutMetrics metrics = getShaper().getTextMetrics(tr, getFTFace(fontID)); + TextLayoutMetrics metrics = getShaper().getTextMetrics(string, getFTFace(fontID)); return width_pixelsToPoints(metrics.width); } @@ -635,10 +814,9 @@ unsigned int iV_GetCountedTextWidth(const char *string, size_t string_length, iV } // Returns the text height *in points* -unsigned int iV_GetTextHeight(const char *string, iV_fonts fontID) +unsigned int iV_GetTextHeight(const char* string, iV_fonts fontID) { - TextRun tr(string, "en", HB_SCRIPT_COMMON, HB_DIRECTION_LTR); - TextLayoutMetrics metrics = getShaper().getTextMetrics(tr, getFTFace(fontID)); + TextLayoutMetrics metrics = getShaper().getTextMetrics(string, getFTFace(fontID)); return height_pixelsToPoints(metrics.height); } @@ -830,7 +1008,8 @@ std::vector iV_FormatText(const char *String, UDWORD MaxWidth, UDWORD return lineDrawResults; } -void iV_DrawTextRotated(const char *string, float XPos, float YPos, float rotation, iV_fonts fontID) +// Needs modification +void iV_DrawTextRotated(const char* string, float XPos, float YPos, float rotation, iV_fonts fontID) { ASSERT_OR_RETURN(, string, "Couldn't render string!"); @@ -846,7 +1025,7 @@ void iV_DrawTextRotated(const char *string, float XPos, float YPos, float rotati color.vector[3] = static_cast(font_colour[3] * 255.f); TextRun tr(string, "en", HB_SCRIPT_COMMON, HB_DIRECTION_LTR); - DrawTextResult drawResult = getShaper().drawText(tr, getFTFace(fontID)); + DrawTextResult drawResult = getShaper().drawText(string, getFTFace(fontID)); if (drawResult.text.width > 0 && drawResult.text.height > 0) { @@ -893,14 +1072,13 @@ void WzText::setText(const std::string &string, iV_fonts fontID/*, bool delayRen drawAndCacheText(string, fontID); } -void WzText::drawAndCacheText(const std::string &string, iV_fonts fontID) +void WzText::drawAndCacheText(const std::string& string, iV_fonts fontID) { mFontID = fontID; mText = string; mRenderingHorizScaleFactor = iV_GetHorizScaleFactor(); mRenderingVertScaleFactor = iV_GetVertScaleFactor(); - TextRun tr(string, "en", HB_SCRIPT_COMMON, HB_DIRECTION_LTR); FTFace &face = getFTFace(fontID); FT_Face &type = face.face(); @@ -908,7 +1086,7 @@ void WzText::drawAndCacheText(const std::string &string, iV_fonts fontID) mPtsLineSize = metricsHeight_PixelsToPoints((type->size->metrics.ascender - type->size->metrics.descender) >> 6); mPtsBelowBase = metricsHeight_PixelsToPoints(type->size->metrics.descender >> 6); - DrawTextResult drawResult = getShaper().drawText(tr, face); + DrawTextResult drawResult = getShaper().drawText(string, face); dimensions = Vector2i(drawResult.text.width, drawResult.text.height); offsets = Vector2i(drawResult.text.offset_x, drawResult.text.offset_y); layoutMetrics = Vector2i(drawResult.layoutMetrics.width, drawResult.layoutMetrics.height); diff --git a/src/seqdisp.cpp b/src/seqdisp.cpp index a525a1b9975..ed47a339e4a 100644 --- a/src/seqdisp.cpp +++ b/src/seqdisp.cpp @@ -430,7 +430,7 @@ bool seq_AddTextForVideo(const char *pText, SDWORD xOffset, SDWORD yOffset, doub { currentLength--; } - currentLength--; + //currentLength--; } memcpy(currentText, pText, currentLength); diff --git a/vcpkg.json b/vcpkg.json index b7f52c30b6b..80c46a85774 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -5,6 +5,7 @@ "sdl2", "physfs", "harfbuzz", + "fribidi", "libogg", "libtheora", "libvorbis",