diff --git a/CMake/Modules/FindHarfBuzz.cmake b/CMake/Modules/FindHarfBuzz.cmake new file mode 100644 index 000000000..af1d597eb --- /dev/null +++ b/CMake/Modules/FindHarfBuzz.cmake @@ -0,0 +1,40 @@ +# Try to find HarfBuzz include and library directories. +# +# The following CMake variables are required: +# HARFBUZZ_DIR - directories to search for the HarfBuzz source directory. +# HARFBUZZ_LIB_DIRS - directories to search for the compiled HarfBuzz library. +# +# After a successful discovery, this will set the following: +# HARFBUZZ_INCLUDE_DIR - directory containing the HarfBuzz header files. +# HARFBUZZ_LIBRARY - the compiled HarfBuzz library. +# In addition, the following IMPORTED target is created: +# harfbuzz::harfbuzz + +# Look for the library in config mode first. +find_package(harfbuzz CONFIG QUIET) +if(TARGET harfbuzz::harfbuzz) + message(STATUS "Found HarfBuzz in config mode.") + set(HARFBUZZ_LIBRARY "harfbuzz::harfbuzz") + return() +else() + message(STATUS "Looking for HarfBuzz in module mode.") +endif() + +find_path(HARFBUZZ_INCLUDE_DIR + NAMES hb.h + HINTS ${HARFBUZZ_DIR} + PATH_SUFFIXES harfbuzz) + +find_library(HARFBUZZ_LIBRARY + NAMES harfbuzz + HINTS ${HARFBUZZ_DIR} ${HARFBUZZ_LIB_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(HarfBuzz DEFAULT_MSG HARFBUZZ_LIBRARY HARFBUZZ_INCLUDE_DIR) + +if(HARFBUZZ_FOUND AND NOT TARGET harfbuzz::harfbuzz) + add_library(harfbuzz::harfbuzz INTERFACE IMPORTED) + set_target_properties(harfbuzz::harfbuzz PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${HARFBUZZ_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${HARFBUZZ_LIBRARY}") +endif() diff --git a/CMake/SampleFileList.cmake b/CMake/SampleFileList.cmake index 75693ada6..627253898 100644 --- a/CMake/SampleFileList.cmake +++ b/CMake/SampleFileList.cmake @@ -96,6 +96,29 @@ set(transform_SRC_FILES ${PROJECT_SOURCE_DIR}/Samples/basic/transform/src/main.cpp ) +set(harfbuzzshaping_HDR_FILES + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFace.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFaceLayer.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFamily.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontGlyph.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontProvider.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.h + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/LanguageData.h +) + +set(harfbuzzshaping_SRC_FILES + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFace.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFaceLayer.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontFamily.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FontProvider.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.cpp + ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/src/main.cpp +) + set(lottie_HDR_FILES ) diff --git a/CMake/gen_samplelists.sh b/CMake/gen_samplelists.sh index 002365a5d..b51d4beb9 100755 --- a/CMake/gen_samplelists.sh +++ b/CMake/gen_samplelists.sh @@ -8,7 +8,7 @@ srcdir='${PROJECT_SOURCE_DIR}' srcpath=Samples samples=( 'shell' 'basic/animation' 'basic/benchmark' 'basic/bitmapfont' 'basic/customlog' 'basic/databinding' 'basic/demo' 'basic/drag' 'basic/loaddocument' 'basic/treeview' 'basic/transform' - 'basic/lottie' 'basic/svg' + 'basic/harfbuzzshaping' 'basic/lottie' 'basic/svg' 'tutorial/template' 'tutorial/drag' 'invaders' 'luainvaders' ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e2f5cb1c..9a18c4de2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,6 +166,7 @@ if(APPLE) endif() option(BUILD_SAMPLES "Build samples" OFF) +option(ENABLE_HARFBUZZ "Enable HarfBuzz for text-shaping sample. Requires the HarfBuzz library." OFF) set(SAMPLES_BACKEND "auto" CACHE STRING "Backend platform and renderer used for the samples.") set_property(CACHE SAMPLES_BACKEND PROPERTY STRINGS auto Win32_GL2 Win32_VK X11_GL2 SDL_GL2 SDL_GL3 SDL_VK SDL_SDLrenderer SFML_GL2 GLFW_GL2 GLFW_GL3 GLFW_VK) @@ -393,6 +394,18 @@ if(NOT NO_FONT_INTERFACE_DEFAULT) endif() endif() +# HarfBuzz +if (ENABLE_HARFBUZZ) + if(NO_FONT_INTERFACE_DEFAULT) + message(FATAL_ERROR "The HarfBuzz sample requires the default (FreeType) font engine to be enabled. Please disable either NO_FONT_INTERFACE_DEFAULT or ENABLE_HARFBUZZ.") + endif() + if(WIN32 AND BUILD_SHARED_LIBS) + message(FATAL_ERROR "-- The HarfBuzz sample cannot be built when using shared libraries on Windows. Please disable either BUILD_SHARED_LIBS or ENABLE_HARFBUZZ.") + endif() + + find_package(HarfBuzz REQUIRED) +endif() + # Lua if(BUILD_LUA_BINDINGS) if(BUILD_LUA_BINDINGS_FOR_LUAJIT) @@ -881,6 +894,9 @@ if(BUILD_SAMPLES) if(ENABLE_SVG_PLUGIN) list(APPEND samples "svg") endif() + if(ENABLE_HARFBUZZ) + list(APPEND samples "harfbuzzshaping") + endif() # Build and install the basic samples foreach(sample ${samples}) @@ -920,6 +936,12 @@ if(BUILD_SAMPLES) BUNDLE DESTINATION ${SAMPLES_DIR}) endif() + # Attach harfbuzz sample dependencies to samples + if (ENABLE_HARFBUZZ) + target_include_directories(harfbuzzshaping PRIVATE ${FREETYPE_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/Source/Core) + target_link_libraries(harfbuzzshaping harfbuzz::harfbuzz) + endif() + # Add assets to emscripten binaries if(EMSCRIPTEN) message("-- Preloading emscipten sample assets") @@ -1034,6 +1056,12 @@ if(BUILD_SAMPLES) DESTINATION ${SAMPLES_DIR}/invaders ) + if (ENABLE_HARFBUZZ AND HARFBUZZ_FOUND) + install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/harfbuzzshaping/data + DESTINATION ${SAMPLES_DIR}/basic/harfbuzzshaping + ) + endif() + if(TARGET lottie) install(DIRECTORY ${PROJECT_SOURCE_DIR}/Samples/basic/lottie/data DESTINATION ${SAMPLES_DIR}/basic/lottie diff --git a/Samples/basic/harfbuzzshaping/data/Cairo-Regular.ttf b/Samples/basic/harfbuzzshaping/data/Cairo-Regular.ttf new file mode 100644 index 000000000..fb9e6cc6b Binary files /dev/null and b/Samples/basic/harfbuzzshaping/data/Cairo-Regular.ttf differ diff --git a/Samples/basic/harfbuzzshaping/data/LICENSE.txt b/Samples/basic/harfbuzzshaping/data/LICENSE.txt new file mode 100644 index 000000000..9a8ed6a18 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/data/LICENSE.txt @@ -0,0 +1,209 @@ +The RmlUi text shaper sample includes fonts that are separately licensed. +They are listed below along with their licenses. + +----------------------------------------------------------- +----------------------------------------------------------- +--- Cairo --- +--- https://fonts.google.com/specimen/Cairo --- +--- --- +--- Cairo-Regular.ttf --- +----------------------------------------------------------- +----------------------------------------------------------- + +Copyright 2009 The Cairo Project Authors (https://github.com/Gue3bara/Cairo) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + + +----------------------------------------------------------- +----------------------------------------------------------- +--- Poppins --- +--- https://fonts.google.com/specimen/Poppins --- +--- --- +--- Poppins-Regular.ttf --- +----------------------------------------------------------- +----------------------------------------------------------- + +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Samples/basic/harfbuzzshaping/data/Poppins-Regular.ttf b/Samples/basic/harfbuzzshaping/data/Poppins-Regular.ttf new file mode 100644 index 000000000..9f0c71b70 Binary files /dev/null and b/Samples/basic/harfbuzzshaping/data/Poppins-Regular.ttf differ diff --git a/Samples/basic/harfbuzzshaping/data/harfbuzzshaping.rml b/Samples/basic/harfbuzzshaping/data/harfbuzzshaping.rml new file mode 100644 index 000000000..d3d30e990 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/data/harfbuzzshaping.rml @@ -0,0 +1,66 @@ + + +Text Shaping Demo + + + + +
+ + + +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Urna neque viverra justo nec ultrices dui sapien eget mi. Risus quis varius quam quisque id. Amet est placerat in egestas erat imperdiet. Velit egestas dui id ornare arcu odio ut sem. Aliquet porttitor lacus luctus accumsan tortor posuere. Et malesuada fames ac turpis egestas integer eget. Enim nunc faucibus a pellentesque sit amet porttitor eget. Nunc pulvinar sapien et ligula. Sit amet mattis vulputate enim nulla aliquet porttitor lacus luctus. Dolor sit amet consectetur adipiscing. Congue eu consequat ac felis donec et odio pellentesque. Nunc non blandit massa enim nec dui nunc mattis.

+

هناك حقيقة مثبتة منذ زمن طويل وهي أن المحتوى المقروء لصفحة ما سيلهي القارئ عن التركيز على الشكل الخارجي للنص أو شكل توضع الفقرات في الصفحة التي يقرأها. ولذلك يتم استخدام طريقة لوريم إيبسوم لأنها تعطي توزيعاَ طبيعياَ -إلى حد ما- للأحرف عوضاً عن استخدام "هنا يوجد محتوى نصي، هنا يوجد محتوى نصي" فتجعلها تبدو (أي الأحرف) وكأنها نص مقروء. العديد من برامح النشر المكتبي وبرامح تحرير صفحات الويب تستخدم لوريم إيبسوم بشكل إفتراضي كنموذج عن النص، وإذا قمت بإدخال "muspi merol" في أي محرك بحث ستظهر العديد من المواقع الحديثة العهد في نتائج البحث. على مدى السنين ظهرت نسخ جديدة ومختلفة من نص لوريم إيبسوم، أحياناً عن طريق الصدفة، وأحياناً عن عمد كإدخال بعض العبارات الفكاهية إليها.

+

यह एक लंबा स्थापित तथ्य है कि जब एक पाठक एक पृष्ठ के खाखे को देखेगा तो पठनीय सामग्री से विचलित हो जाएगा. Lorem Ipsum का उपयोग करने का मुद्दा यह है कि इसमें एक और अधिक या कम अक्षरों का सामान्य वितरण किया गया है, 'Content here, content here' प्रयोग करने की जगह इसे पठनीय English के रूप में प्रयोग किया जाये. अब कई डेस्कटॉप प्रकाशन संकुल और वेब पेज संपादक उनके डिफ़ॉल्ट मॉडल पाठ के रूप में Lorem Ipsum उपयोग करते हैं, और अब "Lorem Ipsum" के लिए खोज अपने शैशव में कई वेब साइटों को उजागर करती है. इसके विभिन्न संस्करणों का वर्षों में विकास हुआ है, कभी दुर्घटना से, तो कभी प्रयोजन पर (हास्य और लगाव डालने के लिए).

+ +
diff --git a/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.cpp b/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.cpp new file mode 100644 index 000000000..4c526f01f --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.cpp @@ -0,0 +1,103 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontEngineInterfaceHarfBuzz.h" +#include "FontFaceHandleHarfBuzz.h" +#include "FontProvider.h" +#include + +FontEngineInterfaceHarfBuzz::FontEngineInterfaceHarfBuzz() +{ + FontProvider::Initialise(); +} + +FontEngineInterfaceHarfBuzz::~FontEngineInterfaceHarfBuzz() +{ + FontProvider::Shutdown(); +} + +bool FontEngineInterfaceHarfBuzz::LoadFontFace(const String& file_name, bool /*fallback_face*/, Style::FontWeight weight) +{ + return FontProvider::LoadFontFace(file_name, weight); +} + +bool FontEngineInterfaceHarfBuzz::LoadFontFace(const byte* data, int data_size, const String& font_family, Style::FontStyle style, + Style::FontWeight weight, bool /*fallback_face*/) +{ + return FontProvider::LoadFontFace(data, data_size, font_family, style, weight); +} + +FontFaceHandle FontEngineInterfaceHarfBuzz::GetFontFaceHandle(const String& family, Style::FontStyle style, Style::FontWeight weight, int size) +{ + auto handle = FontProvider::GetFontFaceHandle(family, style, weight, size); + return reinterpret_cast(handle); +} + +FontEffectsHandle FontEngineInterfaceHarfBuzz::PrepareFontEffects(FontFaceHandle handle, const FontEffectList& font_effects) +{ + auto handle_harfbuzz = reinterpret_cast(handle); + return (FontEffectsHandle)handle_harfbuzz->GenerateLayerConfiguration(font_effects); +} + +const FontMetrics& FontEngineInterfaceHarfBuzz::GetFontMetrics(FontFaceHandle handle) +{ + auto handle_harfbuzz = reinterpret_cast(handle); + return handle_harfbuzz->GetFontMetrics(); +} + +int FontEngineInterfaceHarfBuzz::GetStringWidth(FontFaceHandle handle, const String& string, const TextShapingContext& text_shaping_context, + Character prior_character) +{ + auto handle_harfbuzz = reinterpret_cast(handle); + return handle_harfbuzz->GetStringWidth(string, text_shaping_context, registered_languages, prior_character); +} + +int FontEngineInterfaceHarfBuzz::GenerateString(FontFaceHandle handle, FontEffectsHandle font_effects_handle, const String& string, + const Vector2f& position, const Colourb& colour, float opacity, const TextShapingContext& text_shaping_context, GeometryList& geometry) +{ + auto handle_harfbuzz = reinterpret_cast(handle); + return handle_harfbuzz->GenerateString(geometry, string, position, colour, opacity, text_shaping_context, registered_languages, + (int)font_effects_handle); +} + +int FontEngineInterfaceHarfBuzz::GetVersion(FontFaceHandle handle) +{ + auto handle_harfbuzz = reinterpret_cast(handle); + return handle_harfbuzz->GetVersion(); +} + +void FontEngineInterfaceHarfBuzz::ReleaseFontResources() +{ + FontProvider::ReleaseFontResources(); +} + +void FontEngineInterfaceHarfBuzz::RegisterLanguage(const String& language_bcp47_code, const String& script_iso15924_code, + const TextFlowDirection text_flow_direction) +{ + registered_languages[language_bcp47_code] = LanguageData{script_iso15924_code, text_flow_direction}; +} diff --git a/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.h b/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.h new file mode 100644 index 000000000..c03790ec6 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontEngineInterfaceHarfBuzz.h @@ -0,0 +1,91 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTENGINEINTERFACEHARFBUZZ_H +#define FONTENGINEINTERFACEHARFBUZZ_H + +#include "LanguageData.h" +#include + +using Rml::byte; +using Rml::Character; +using Rml::Colourb; +using Rml::FontFaceHandle; +using Rml::FontEffectList; +using Rml::FontEffectsHandle; +using Rml::FontMetrics; +using Rml::GeometryList; +using Rml::String; +using Rml::TextShapingContext; +using Rml::Vector2f; +namespace Style = Rml::Style; + +class FontEngineInterfaceHarfBuzz : public Rml::FontEngineInterface { +public: + FontEngineInterfaceHarfBuzz(); + virtual ~FontEngineInterfaceHarfBuzz(); + + /// Adds a new font face to the database. The face's family, style and weight will be determined from the face itself. + bool LoadFontFace(const String& file_name, bool fallback_face, Style::FontWeight weight) override; + + /// Adds a new font face to the database using the provided family, style and weight. + bool LoadFontFace(const byte* data, int data_size, const String& font_family, Style::FontStyle style, Style::FontWeight weight, + bool fallback_face) override; + + /// Returns a handle to a font face that can be used to position and render text. This will return the closest match + /// it can find, but in the event a font family is requested that does not exist, NULL will be returned instead of a + /// valid handle. + FontFaceHandle GetFontFaceHandle(const String& family, Style::FontStyle style, Style::FontWeight weight, int size) override; + + /// Prepares for font effects by configuring a new, or returning an existing, layer configuration. + FontEffectsHandle PrepareFontEffects(FontFaceHandle, const FontEffectList& font_effects) override; + + /// Returns the font metrics of the given font face. + const FontMetrics& GetFontMetrics(FontFaceHandle handle) override; + + /// Returns the width a string will take up if rendered with this handle. + int GetStringWidth(FontFaceHandle, const String& string, const TextShapingContext& text_shaping_context, Character prior_character) override; + + /// Generates the geometry required to render a single line of text. + int GenerateString(FontFaceHandle, FontEffectsHandle, const String& string, const Vector2f& position, const Colourb& colour, float opacity, + const TextShapingContext& text_shaping_context, GeometryList& geometry) override; + + /// Returns the current version of the font face. + int GetVersion(FontFaceHandle handle) override; + + /// Releases resources owned by sized font faces, including their textures and rendered glyphs. + void ReleaseFontResources() override; + + /// Registers a new language to assist with text shaping. + void RegisterLanguage(const String& language_bcp47_code, const String& script_iso15924_code, const TextFlowDirection text_flow_direction); + +private: + LanguageDataMap registered_languages; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontFace.cpp b/Samples/basic/harfbuzzshaping/src/FontFace.cpp new file mode 100644 index 000000000..a8a0f6f6c --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFace.cpp @@ -0,0 +1,87 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontFace.h" +#include "FontEngineDefault/FreeTypeInterface.h" + +FontFace::FontFace(FontFaceHandleFreetype _face, Style::FontStyle _style, Style::FontWeight _weight) +{ + style = _style; + weight = _weight; + face = _face; +} + +FontFace::~FontFace() +{ + if (face) + Rml::FreeType::ReleaseFace(face); +} + +Style::FontStyle FontFace::GetStyle() const +{ + return style; +} + +Style::FontWeight FontFace::GetWeight() const +{ + return weight; +} + +FontFaceHandleHarfBuzz* FontFace::GetHandle(int size, bool load_default_glyphs) +{ + auto it = handles.find(size); + if (it != handles.end()) + return it->second.get(); + + // See if this face has been released. + if (!face) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Font face has been released, unable to generate new handle."); + return nullptr; + } + + // Construct and initialise the new handle. + auto handle = Rml::MakeUnique(); + if (!handle->Initialize(face, size, load_default_glyphs)) + { + handles[size] = nullptr; + return nullptr; + } + + FontFaceHandleHarfBuzz* result = handle.get(); + + // Save the new handle to the font face + handles[size] = std::move(handle); + + return result; +} + +void FontFace::ReleaseFontResources() +{ + HandleMap().swap(handles); +} diff --git a/Samples/basic/harfbuzzshaping/src/FontFace.h b/Samples/basic/harfbuzzshaping/src/FontFace.h new file mode 100644 index 000000000..c9e85c4dd --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFace.h @@ -0,0 +1,74 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTFACE_H +#define FONTFACE_H + +#include "FontEngineDefault/FontTypes.h" +#include "FontFaceHandleHarfBuzz.h" +#include + +using Rml::FontFaceHandleFreetype; +using Rml::UniquePtr; +using Rml::UnorderedMap; +namespace Style = Rml::Style; + +/** + Original author: Peter Curry + Modified to support HarfBuzz text shaping. + */ + +class FontFace { +public: + FontFace(FontFaceHandleFreetype face, Style::FontStyle style, Style::FontWeight weight); + ~FontFace(); + + Style::FontStyle GetStyle() const; + Style::FontWeight GetWeight() const; + + /// Returns a handle for positioning and rendering this face at the given size. + /// @param[in] size The size of the desired handle, in points. + /// @param[in] load_default_glyphs True to load the default set of glyph (ASCII range). + /// @return The font handle. + FontFaceHandleHarfBuzz* GetHandle(int size, bool load_default_glyphs); + + /// Releases resources owned by sized font faces, including their textures and rendered glyphs. + void ReleaseFontResources(); + +private: + Style::FontStyle style; + Style::FontWeight weight; + + // Key is font size + using HandleMap = UnorderedMap>; + HandleMap handles; + + FontFaceHandleFreetype face; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.cpp b/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.cpp new file mode 100644 index 000000000..bfb660017 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.cpp @@ -0,0 +1,552 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontFaceHandleHarfBuzz.h" +#include "FontEngineDefault/FreeTypeInterface.h" +#include "FontFaceLayer.h" +#include "FontProvider.h" +#include "FreeTypeInterface.h" +#include +#include FT_FREETYPE_H +#include +#include +#include + +static bool IsASCIIControlCharacter(Character c) +{ + return (char32_t)c < ' '; +} + +FontFaceHandleHarfBuzz::FontFaceHandleHarfBuzz() +{ + base_layer = nullptr; + metrics = {}; + ft_face = 0; + hb_font = nullptr; +} + +FontFaceHandleHarfBuzz::~FontFaceHandleHarfBuzz() +{ + hb_font_destroy(hb_font); + + glyphs.clear(); + layers.clear(); +} + +bool FontFaceHandleHarfBuzz::Initialize(FontFaceHandleFreetype face, int font_size, bool load_default_glyphs) +{ + ft_face = face; + + RMLUI_ASSERTMSG(layer_configurations.empty(), "Initialize must only be called once."); + + if (!FreeType::InitialiseFaceHandle(ft_face, font_size, glyphs, metrics, load_default_glyphs)) + return false; + + has_kerning = Rml::FreeType::HasKerning(ft_face); + FillKerningPairCache(); + + hb_font = hb_ft_font_create_referenced((FT_Face)ft_face); + RMLUI_ASSERT(hb_font != nullptr); + hb_font_set_ptem(hb_font, (float)font_size); + + // Generate the default layer and layer configuration. + base_layer = GetOrCreateLayer(nullptr); + layer_configurations.push_back(LayerConfiguration{base_layer}); + + return true; +} + +const FontMetrics& FontFaceHandleHarfBuzz::GetFontMetrics() const +{ + return metrics; +} + +const FontGlyphMap& FontFaceHandleHarfBuzz::GetGlyphs() const +{ + return glyphs; +} + +int FontFaceHandleHarfBuzz::GetStringWidth(const String& string, const TextShapingContext& text_shaping_context, + const LanguageDataMap& registered_languages, Character prior_character) +{ + int width = 0; + + // Apply text shaping. + hb_buffer_t* shaping_buffer = hb_buffer_create(); + RMLUI_ASSERT(shaping_buffer != nullptr); + ConfigureTextShapingBuffer(shaping_buffer, string, text_shaping_context, registered_languages); + hb_buffer_add_utf8(shaping_buffer, string.c_str(), -1, 0, -1); + hb_shape(hb_font, shaping_buffer, nullptr, 0); + + FontGlyphIndex prior_glyph_codepoint = FreeType::GetGlyphIndexFromCharacter(ft_face, prior_character); + + unsigned int glyph_count = 0; + hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(shaping_buffer, &glyph_count); + + for (int g = 0; g < (int)glyph_count; ++g) + { + // Don't render control characters. + Character character = *Rml::StringIteratorU8(&string[glyph_info[g].cluster]); + if (IsASCIIControlCharacter(character)) + continue; + + FontGlyphIndex glyph_codepoint = glyph_info[g].codepoint; + const FontGlyph* glyph = GetOrAppendGlyph(glyph_codepoint); + if (!glyph) + continue; + + // Adjust the cursor for the kerning between this character and the previous one. + width += GetKerning(prior_glyph_codepoint, glyph_codepoint); + + // Adjust the cursor for this character's advance. + width += glyph->advance; + width += (int)text_shaping_context.letter_spacing; + + prior_glyph_codepoint = glyph_codepoint; + } + + hb_buffer_destroy(shaping_buffer); + + return Rml::Math::Max(width, 0); +} + +int FontFaceHandleHarfBuzz::GenerateLayerConfiguration(const FontEffectList& font_effects) +{ + if (font_effects.empty()) + return 0; + + // Check each existing configuration for a match with this arrangement of effects. + int configuration_index = 1; + for (; configuration_index < (int)layer_configurations.size(); ++configuration_index) + { + const LayerConfiguration& configuration = layer_configurations[configuration_index]; + + // Check the size is correct. For a match, there should be one layer in the configuration + // plus an extra for the base layer. + if (configuration.size() != font_effects.size() + 1) + continue; + + // Check through each layer, checking it was created by the same effect as the one we're + // checking. + size_t effect_index = 0; + for (size_t i = 0; i < configuration.size(); ++i) + { + // Skip the base layer ... + if (configuration[i]->GetFontEffect() == nullptr) + continue; + + // If the ith layer's effect doesn't match the equivalent effect, then this + // configuration can't match. + if (configuration[i]->GetFontEffect() != font_effects[effect_index].get()) + break; + + // Check the next one ... + ++effect_index; + } + + if (effect_index == font_effects.size()) + return configuration_index; + } + + // No match, so we have to generate a new layer configuration. + layer_configurations.push_back(LayerConfiguration()); + LayerConfiguration& layer_configuration = layer_configurations.back(); + + bool added_base_layer = false; + + for (size_t i = 0; i < font_effects.size(); ++i) + { + if (!added_base_layer && font_effects[i]->GetLayer() == FontEffect::Layer::Front) + { + layer_configuration.push_back(base_layer); + added_base_layer = true; + } + + FontFaceLayer* new_layer = GetOrCreateLayer(font_effects[i]); + layer_configuration.push_back(new_layer); + } + + // Add the base layer now if we still haven't added it. + if (!added_base_layer) + layer_configuration.push_back(base_layer); + + return (int)(layer_configurations.size() - 1); +} + +bool FontFaceHandleHarfBuzz::GenerateLayerTexture(UniquePtr& texture_data, Vector2i& texture_dimensions, + const FontEffect* font_effect, int texture_id, int handle_version) const +{ + if (handle_version != version) + { + RMLUI_ERRORMSG("While generating font layer texture: Handle version mismatch in texture vs font-face."); + return false; + } + + auto it = std::find_if(layers.begin(), layers.end(), [font_effect](const EffectLayerPair& pair) { return pair.font_effect == font_effect; }); + + if (it == layers.end()) + { + RMLUI_ERRORMSG("While generating font layer texture: Layer id not found."); + return false; + } + + return it->layer->GenerateTexture(texture_data, texture_dimensions, texture_id, glyphs); +} + +int FontFaceHandleHarfBuzz::GenerateString(GeometryList& geometry, const String& string, const Vector2f position, const Colourb colour, + const float opacity, const TextShapingContext& text_shaping_context, const LanguageDataMap& registered_languages, + const int layer_configuration_index) +{ + int geometry_index = 0; + int line_width = 0; + + RMLUI_ASSERT(layer_configuration_index >= 0); + RMLUI_ASSERT(layer_configuration_index < (int)layer_configurations.size()); + + UpdateLayersOnDirty(); + + // Fetch the requested configuration and generate the geometry for each one. + const LayerConfiguration& layer_configuration = layer_configurations[layer_configuration_index]; + + // Reserve for the common case of one texture per layer. + geometry.reserve(layer_configuration.size()); + + hb_buffer_t* shaping_buffer = hb_buffer_create(); + RMLUI_ASSERT(shaping_buffer != nullptr); + + for (size_t i = 0; i < layer_configuration.size(); ++i) + { + FontFaceLayer* layer = layer_configuration[i]; + + Colourb layer_colour; + if (layer == base_layer) + { + layer_colour = colour; + } + else + { + layer_colour = layer->GetColour(); + if (opacity < 1.f) + layer_colour.alpha = byte(opacity * float(layer_colour.alpha)); + } + + const int num_textures = layer->GetNumTextures(); + + if (num_textures == 0) + continue; + + // Resize the geometry list if required. + if ((int)geometry.size() < geometry_index + num_textures) + geometry.resize(geometry_index + num_textures); + + RMLUI_ASSERT(geometry_index < (int)geometry.size()); + + // Bind the textures to the geometries. + for (int tex_index = 0; tex_index < num_textures; ++tex_index) + geometry[geometry_index + tex_index].SetTexture(layer->GetTexture(tex_index)); + + line_width = 0; + FontGlyphIndex prior_glyph_codepoint = 0; + + // Set up and apply text shaping. + hb_buffer_clear_contents(shaping_buffer); + ConfigureTextShapingBuffer(shaping_buffer, string, text_shaping_context, registered_languages); + hb_buffer_add_utf8(shaping_buffer, string.c_str(), -1, 0, -1); + hb_shape(hb_font, shaping_buffer, nullptr, 0); + + unsigned int glyph_count = 0; + hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(shaping_buffer, &glyph_count); + + geometry[geometry_index].GetIndices().reserve(glyph_count * 6); + geometry[geometry_index].GetVertices().reserve(glyph_count * 4); + + for (int g = 0; g < (int)glyph_count; ++g) + { + // Don't render control characters. + Character character = *Rml::StringIteratorU8(&string[glyph_info[g].cluster]); + if (IsASCIIControlCharacter(character)) + continue; + + FontGlyphIndex glyph_codepoint = glyph_info[g].codepoint; + const FontGlyph* glyph = GetOrAppendGlyph(glyph_codepoint); + if (!glyph) + continue; + + // Adjust the cursor for the kerning between this character and the previous one. + line_width += GetKerning(prior_glyph_codepoint, glyph_codepoint); + + // Use white vertex colors on RGB glyphs. + const Colourb glyph_color = + (layer == base_layer && glyph->color_format == Rml::ColorFormat::RGBA8 ? Colourb(255, layer_colour.alpha) : layer_colour); + + layer->GenerateGeometry(&geometry[geometry_index], glyph_codepoint, Vector2f(position.x + line_width, position.y), + glyph_color); + + line_width += glyph->advance; + line_width += (int)text_shaping_context.letter_spacing; + prior_glyph_codepoint = glyph_codepoint; + } + + geometry_index += num_textures; + } + + hb_buffer_destroy(shaping_buffer); + + // Cull any excess geometry from a previous generation. + geometry.resize(geometry_index); + + return Rml::Math::Max(line_width, 0); +} + +bool FontFaceHandleHarfBuzz::UpdateLayersOnDirty() +{ + bool result = false; + + // If we are dirty, regenerate all the layers and increment the version + if (is_layers_dirty && base_layer) + { + is_layers_dirty = false; + ++version; + + // Regenerate all the layers. + // Note: The layer regeneration needs to happen in the order in which the layers were created, + // otherwise we may end up cloning a layer which has not yet been regenerated. This means trouble! + for (auto& pair : layers) + { + GenerateLayer(pair.layer.get()); + } + + result = true; + } + + return result; +} + +int FontFaceHandleHarfBuzz::GetVersion() const +{ + return version; +} + +bool FontFaceHandleHarfBuzz::AppendGlyph(FontGlyphIndex glyph_index) +{ + bool result = FreeType::AppendGlyph(ft_face, metrics.size, glyph_index, glyphs); + return result; +} + +void FontFaceHandleHarfBuzz::FillKerningPairCache() +{ + if (!has_kerning) + return; + + static constexpr char32_t KerningCache_AsciiSubsetBegin = 32; + static constexpr char32_t KerningCache_AsciiSubsetLast = 126; + + for (char32_t i = KerningCache_AsciiSubsetBegin; i <= KerningCache_AsciiSubsetLast; i++) + { + for (char32_t j = KerningCache_AsciiSubsetBegin; j <= KerningCache_AsciiSubsetLast; j++) + { + const bool first_iteration = (i == KerningCache_AsciiSubsetBegin && j == KerningCache_AsciiSubsetBegin); + + // Fetch the kerning from the font face. Submit zero font size on subsequent iterations for performance reasons. + const int kerning = FreeType::GetKerning(ft_face, first_iteration ? metrics.size : 0, + FreeType::GetGlyphIndexFromCharacter(ft_face, Character(i)), FreeType::GetGlyphIndexFromCharacter(ft_face, Character(j))); + if (kerning != 0) + { + kerning_pair_cache.emplace(AsciiPair((i << 8) | j), KerningIntType(kerning)); + } + } + } +} + +int FontFaceHandleHarfBuzz::GetKerning(FontGlyphIndex lhs, FontGlyphIndex rhs) const +{ + // Check if we have no kerning, or if we are have an unsupported character. + if (!has_kerning || lhs == 0 || rhs == 0) + return 0; + + // See if the kerning pair has been cached. + const auto it = kerning_pair_cache.find(AsciiPair((int(lhs) << 8) | int(rhs))); + if (it != kerning_pair_cache.end()) + { + return it->second; + } + + // Fetch it from the font face instead. + const int result = FreeType::GetKerning(ft_face, metrics.size, lhs, rhs); + return result; +} + +const FontGlyph* FontFaceHandleHarfBuzz::GetOrAppendGlyph(FontGlyphIndex& glyph_index) +{ + auto glyph_location = glyphs.find(glyph_index); + if (glyph_location == glyphs.cend()) + { + bool result = false; + if (glyph_index != 0) + result = AppendGlyph(glyph_index); + + if (result) + { + glyph_location = glyphs.find(glyph_index); + if (glyph_location == glyphs.cend()) + { + RMLUI_ERROR; + return nullptr; + } + + is_layers_dirty = true; + } + else + return nullptr; + } + + const FontGlyph* glyph = &glyph_location->second; + return glyph; +} + +FontFaceLayer* FontFaceHandleHarfBuzz::GetOrCreateLayer(const SharedPtr& font_effect) +{ + // Search for the font effect layer first, it may have been instanced before as part of a different configuration. + const FontEffect* font_effect_ptr = font_effect.get(); + auto it = + std::find_if(layers.begin(), layers.end(), [font_effect_ptr](const EffectLayerPair& pair) { return pair.font_effect == font_effect_ptr; }); + + if (it != layers.end()) + return it->layer.get(); + + // No existing effect matches, generate a new layer for the effect. + layers.push_back(EffectLayerPair{font_effect_ptr, nullptr}); + auto& layer = layers.back().layer; + + layer = Rml::MakeUnique(font_effect); + GenerateLayer(layer.get()); + + return layer.get(); +} + +bool FontFaceHandleHarfBuzz::GenerateLayer(FontFaceLayer* layer) +{ + RMLUI_ASSERT(layer); + const FontEffect* font_effect = layer->GetFontEffect(); + bool result = false; + + if (!font_effect) + { + result = layer->Generate(this); + } + else + { + // Determine which, if any, layer the new layer should copy its geometry and textures from. + FontFaceLayer* clone = nullptr; + bool clone_glyph_origins = true; + String generation_key; + size_t fingerprint = font_effect->GetFingerprint(); + + if (!font_effect->HasUniqueTexture()) + { + clone = base_layer; + clone_glyph_origins = false; + } + else + { + auto cache_iterator = layer_cache.find(fingerprint); + if (cache_iterator != layer_cache.end() && cache_iterator->second != layer) + clone = cache_iterator->second; + } + + // Create a new layer. + result = layer->Generate(this, clone, clone_glyph_origins); + + // Cache the layer in the layer cache if it generated its own textures (ie, didn't clone). + if (!clone) + layer_cache[fingerprint] = layer; + } + + return result; +} + +void FontFaceHandleHarfBuzz::ConfigureTextShapingBuffer(hb_buffer_t* shaping_buffer, const String& string, + const TextShapingContext& text_shaping_context, const LanguageDataMap& registered_languages) +{ + // Set the buffer's language based on the value of the element's 'lang' attribute. + hb_buffer_set_language(shaping_buffer, hb_language_from_string(text_shaping_context.language.c_str(), -1)); + + // Set the buffer's script. + hb_script_t script = HB_SCRIPT_UNKNOWN; + auto registered_language_location = registered_languages.find(text_shaping_context.language); + if (registered_language_location != registered_languages.cend()) + // Get script from registered language data. + script = hb_script_from_string(registered_language_location->second.script_code.c_str(), -1); + else + { + // Try to guess script from the first character of the string. + hb_unicode_funcs_t* unicode_functions = hb_unicode_funcs_get_default(); + if (unicode_functions != nullptr && !string.empty()) + { + Character first_character = *Rml::StringIteratorU8(string); + script = hb_unicode_script(unicode_functions, (hb_codepoint_t)first_character); + } + } + + hb_buffer_set_script(shaping_buffer, script); + + // Set the buffer's text-flow direction based on the value of the element's 'dir' attribute. + switch (text_shaping_context.text_direction) + { + case Rml::Style::Direction::Auto: + if (registered_language_location != registered_languages.cend()) + // Automatically determine the text-flow direction from the registered language. + switch (registered_language_location->second.text_flow_direction) + { + case TextFlowDirection::LeftToRight: hb_buffer_set_direction(shaping_buffer, HB_DIRECTION_LTR); break; + case TextFlowDirection::RightToLeft: hb_buffer_set_direction(shaping_buffer, HB_DIRECTION_RTL); break; + } + else + { + // Language not registered; determine best text-flow direction based on script. + bool is_common_right_to_left_script = + script == HB_SCRIPT_ARABIC || script == HB_SCRIPT_HEBREW || script == HB_SCRIPT_SYRIAC || script == HB_SCRIPT_THAANA; + hb_buffer_set_direction(shaping_buffer, is_common_right_to_left_script ? HB_DIRECTION_RTL : HB_DIRECTION_LTR); + } + break; + + case Rml::Style::Direction::Ltr: hb_buffer_set_direction(shaping_buffer, HB_DIRECTION_LTR); break; + case Rml::Style::Direction::Rtl: hb_buffer_set_direction(shaping_buffer, HB_DIRECTION_RTL); break; + } + + // Set buffer flags for additional text-shaping configuration. + int buffer_flags = HB_BUFFER_FLAG_DEFAULT | HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; + if (script == HB_SCRIPT_ARABIC) + buffer_flags |= HB_BUFFER_FLAG_PRODUCE_SAFE_TO_INSERT_TATWEEL; + +#ifdef RMLUI_DEBUG + buffer_flags |= HB_BUFFER_FLAG_VERIFY; +#endif + + hb_buffer_set_flags(shaping_buffer, (hb_buffer_flags_t)buffer_flags); +} diff --git a/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.h b/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.h new file mode 100644 index 000000000..2e6679873 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFaceHandleHarfBuzz.h @@ -0,0 +1,175 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTFACEHANDLEHARFBUZZ_H +#define FONTFACEHANDLEHARFBUZZ_H + +#include "FontEngineDefault/FontTypes.h" +#include "FontFaceLayer.h" +#include "FontGlyph.h" +#include "LanguageData.h" +#include + +using Rml::byte; +using Rml::Character; +using Rml::Colourb; +using Rml::FontEffect; +using Rml::FontEffectList; +using Rml::FontFaceHandleFreetype; +using Rml::FontGlyph; +using Rml::FontMetrics; +using Rml::GeometryList; +using Rml::SharedPtr; +using Rml::SmallUnorderedMap; +using Rml::String; +using Rml::TextShapingContext; +using Rml::UniquePtr; +using Rml::UnorderedMap; +using Rml::Vector; +using Rml::Vector2f; +using Rml::Vector2i; + +/** + Original author: Peter Curry + Modified to support HarfBuzz text shaping. + */ +class FontFaceHandleHarfBuzz : public Rml::NonCopyMoveable { +public: + FontFaceHandleHarfBuzz(); + ~FontFaceHandleHarfBuzz(); + + bool Initialize(FontFaceHandleFreetype face, int font_size, bool load_default_glyphs); + + const FontMetrics& GetFontMetrics() const; + + const FontGlyphMap& GetGlyphs() const; + + /// Returns the width a string will take up if rendered with this handle. + /// @param[in] string The string to measure. + /// @param[in] text_shaping_context Extra parameters that provide context for text shaping. + /// @param[in] registered_languages A list of languages registered in the font engine interface. + /// @param[in] prior_character The optionally-specified character that immediately precedes the string. This may have an impact on the string + /// width due to kerning. + /// @return The width, in pixels, this string will occupy if rendered with this handle. + int GetStringWidth(const String& string, const TextShapingContext& text_shaping_context, const LanguageDataMap& registered_languages, + Character prior_character = Character::Null); + + /// Generates, if required, the layer configuration for a given list of font effects. + /// @param[in] font_effects The list of font effects to generate the configuration for. + /// @return The index to use when generating geometry using this configuration. + int GenerateLayerConfiguration(const FontEffectList& font_effects); + /// Generates the texture data for a layer (for the texture database). + /// @param[out] texture_data The pointer to be set to the generated texture data. + /// @param[out] texture_dimensions The dimensions of the texture. + /// @param[in] font_effect The font effect used for the layer. + /// @param[in] texture_id The index of the texture within the layer to generate. + /// @param[in] handle_version The version of the handle data. Function returns false if out of date. + bool GenerateLayerTexture(UniquePtr& texture_data, Vector2i& texture_dimensions, const FontEffect* font_effect, int texture_id, + int handle_version) const; + + /// Generates the geometry required to render a single line of text. + /// @param[out] geometry An array of geometries to generate the geometry into. + /// @param[in] string The string to render. + /// @param[in] position The position of the baseline of the first character to render. + /// @param[in] colour The colour to render the text. + /// @param[in] opacity The opacity of the text, should be applied to font effects. + /// @param[in] text_shaping_context Extra parameters that provide context for text shaping. + /// @param[in] registered_languages A list of languages registered in the font engine interface. + /// @param[in] layer_configuration Face configuration index to use for generating string. + /// @return The width, in pixels, of the string geometry. + int GenerateString(GeometryList& geometry, const String& string, Vector2f position, Colourb colour, float opacity, + const TextShapingContext& text_shaping_context, const LanguageDataMap& registered_languages, int layer_configuration = 0); + + /// Version is changed whenever the layers are dirtied, requiring regeneration of string geometry. + int GetVersion() const; + +private: + // Build and append glyph to 'glyphs' + bool AppendGlyph(FontGlyphIndex glyph_index); + + // Build a kerning cache for common characters. + void FillKerningPairCache(); + + // Return the kerning for a codepoint pair. + int GetKerning(FontGlyphIndex lhs, FontGlyphIndex rhs) const; + + /// Retrieve a glyph from the given code index, building and appending a new glyph if not already built. + /// @param[in-out] glyph_index The glyph index, can be changed e.g. to the replacement character if no glyph is found. + /// @return The font glyph for the returned glyph index. + const FontGlyph* GetOrAppendGlyph(FontGlyphIndex& glyph_index); + + // Regenerate layers if dirty, such as after adding new glyphs. + bool UpdateLayersOnDirty(); + + // Create a new layer from the given font effect if it does not already exist. + FontFaceLayer* GetOrCreateLayer(const SharedPtr& font_effect); + + // (Re-)generate a layer in this font face handle. + bool GenerateLayer(FontFaceLayer* layer); + + // Configure internal text shaping buffer values with context. + void ConfigureTextShapingBuffer(struct hb_buffer_t* shaping_buffer, const String& string, const TextShapingContext& text_shaping_context, + const LanguageDataMap& registered_languages); + + FontGlyphMap glyphs; + + struct EffectLayerPair { + const FontEffect* font_effect; + UniquePtr layer; + }; + using FontLayerMap = Vector; + using FontLayerCache = SmallUnorderedMap; + using LayerConfiguration = Vector; + using LayerConfigurationList = Vector; + + // The list of all font layers, index by the effect that instanced them. + FontFaceLayer* base_layer; + FontLayerMap layers; + // Each font layer that generated geometry or textures, indexed by the font-effect's fingerprint key. + FontLayerCache layer_cache; + + // Pre-cache kerning pairs for some ascii subset of all characters. + using AsciiPair = uint16_t; + using KerningIntType = int16_t; + using KerningPairs = UnorderedMap; + KerningPairs kerning_pair_cache; + + bool has_kerning = false; + bool is_layers_dirty = false; + int version = 0; + + // All configurations currently in use on this handle. New configurations will be generated as required. + LayerConfigurationList layer_configurations; + + FontMetrics metrics; + + FontFaceHandleFreetype ft_face; + struct hb_font_t* hb_font; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontFaceLayer.cpp b/Samples/basic/harfbuzzshaping/src/FontFaceLayer.cpp new file mode 100644 index 000000000..ab5c4ca1f --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFaceLayer.cpp @@ -0,0 +1,289 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontFaceLayer.h" +#include "FontFaceHandleHarfBuzz.h" +#include + +FontFaceLayer::FontFaceLayer(const SharedPtr& _effect) : colour(255, 255, 255) +{ + effect = _effect; + if (effect) + colour = effect->GetColour(); +} + +FontFaceLayer::~FontFaceLayer() {} + +bool FontFaceLayer::Generate(const FontFaceHandleHarfBuzz* handle, const FontFaceLayer* clone, bool clone_glyph_origins) +{ + // Clear the old layout if it exists. + { + // @performance: We could be much smarter about this, e.g. such as adding new glyphs to the existing texture layout and textures. + // Right now we re-generate the whole thing, including textures. + texture_layout = TextureLayout{}; + character_boxes.clear(); + textures.clear(); + } + + const FontGlyphMap& glyphs = handle->GetGlyphs(); + + // Generate the new layout. + if (clone) + { + // Clone the geometry and textures from the clone layer. + character_boxes = clone->character_boxes; + + // Copy the cloned layer's textures. + for (size_t i = 0; i < clone->textures.size(); ++i) + textures.push_back(clone->textures[i]); + + // Request the effect (if we have one) and adjust the origins as appropriate. + if (effect && !clone_glyph_origins) + { + for (auto& pair : glyphs) + { + FontGlyphIndex glyph_index = pair.first; + const FontGlyph& glyph = pair.second; + + auto it = character_boxes.find(glyph_index); + if (it == character_boxes.end()) + { + // This can happen if the layers have been dirtied in FontHandleDefault. We will + // probably be regenerated soon, just skip the character for now. + continue; + } + + TextureBox& box = it->second; + + Vector2i glyph_origin = Vector2i(box.origin); + Vector2i glyph_dimensions = Vector2i(box.dimensions); + + if (effect->GetGlyphMetrics(glyph_origin, glyph_dimensions, glyph)) + box.origin = Vector2f(glyph_origin); + else + box.texture_index = -1; + } + } + } + else + { + // Initialise the texture layout for the glyphs. + character_boxes.reserve(glyphs.size()); + for (auto& pair : glyphs) + { + FontGlyphIndex glyph_index = pair.first; + const FontGlyph& glyph = pair.second; + + Vector2i glyph_origin(0, 0); + Vector2i glyph_dimensions = glyph.bitmap_dimensions; + + // Adjust glyph origin / dimensions for the font effect. + if (effect) + { + if (!effect->GetGlyphMetrics(glyph_origin, glyph_dimensions, glyph)) + continue; + } + + TextureBox box; + box.origin = Vector2f(float(glyph_origin.x + glyph.bearing.x), float(glyph_origin.y - glyph.bearing.y)); + box.dimensions = Vector2f(glyph_dimensions); + + RMLUI_ASSERT(box.dimensions.x >= 0 && box.dimensions.y >= 0); + + character_boxes[glyph_index] = box; + + // Add the character's dimensions into the texture layout engine. + texture_layout.AddRectangle((int)glyph_index, glyph_dimensions); + } + + constexpr int max_texture_dimensions = 1024; + + // Generate the texture layout; this will position the glyph rectangles efficiently and + // allocate the texture data ready for writing. + if (!texture_layout.GenerateLayout(max_texture_dimensions)) + return false; + + // Iterate over each rectangle in the layout, copying the glyph data into the rectangle as + // appropriate and generating geometry. + for (int i = 0; i < texture_layout.GetNumRectangles(); ++i) + { + Rml::TextureLayoutRectangle& rectangle = texture_layout.GetRectangle(i); + const Rml::TextureLayoutTexture& texture = texture_layout.GetTexture(rectangle.GetTextureIndex()); + FontGlyphIndex glyph_index = (FontGlyphIndex)rectangle.GetId(); + RMLUI_ASSERT(character_boxes.find(glyph_index) != character_boxes.end()); + TextureBox& box = character_boxes[glyph_index]; + + // Set the character's texture index. + box.texture_index = rectangle.GetTextureIndex(); + + // Generate the character's texture coordinates. + box.texcoords[0].x = float(rectangle.GetPosition().x) / float(texture.GetDimensions().x); + box.texcoords[0].y = float(rectangle.GetPosition().y) / float(texture.GetDimensions().y); + box.texcoords[1].x = float(rectangle.GetPosition().x + rectangle.GetDimensions().x) / float(texture.GetDimensions().x); + box.texcoords[1].y = float(rectangle.GetPosition().y + rectangle.GetDimensions().y) / float(texture.GetDimensions().y); + } + + const FontEffect* effect_ptr = effect.get(); + const int handle_version = handle->GetVersion(); + + // Generate the textures. + for (int i = 0; i < texture_layout.GetNumTextures(); ++i) + { + const int texture_id = i; + + Rml::TextureCallback texture_callback = [handle, effect_ptr, texture_id, handle_version](Rml::RenderInterface* render_interface, + const String& /*name*/, Rml::TextureHandle& out_texture_handle, Vector2i& out_dimensions) -> bool { + UniquePtr data; + if (!handle->GenerateLayerTexture(data, out_dimensions, effect_ptr, texture_id, handle_version) || !data) + return false; + if (!render_interface->GenerateTexture(out_texture_handle, data.get(), out_dimensions)) + return false; + return true; + }; + + Texture texture; + texture.Set("font-face-layer", texture_callback); + textures.push_back(texture); + } + } + + return true; +} + +bool FontFaceLayer::GenerateTexture(UniquePtr& texture_data, Vector2i& texture_dimensions, int texture_id, const FontGlyphMap& glyphs) +{ + if (texture_id < 0 || texture_id > texture_layout.GetNumTextures()) + return false; + + // Generate the texture data. + texture_data = texture_layout.GetTexture(texture_id).AllocateTexture(); + texture_dimensions = texture_layout.GetTexture(texture_id).GetDimensions(); + + for (int i = 0; i < texture_layout.GetNumRectangles(); ++i) + { + Rml::TextureLayoutRectangle& rectangle = texture_layout.GetRectangle(i); + FontGlyphIndex glyph_index = (FontGlyphIndex)rectangle.GetId(); + RMLUI_ASSERT(character_boxes.find(glyph_index) != character_boxes.end()); + + TextureBox& box = character_boxes[glyph_index]; + + if (box.texture_index != texture_id) + continue; + + auto it = glyphs.find((FontGlyphIndex)rectangle.GetId()); + if (it == glyphs.end()) + continue; + + const FontGlyph& glyph = it->second; + + if (effect == nullptr) + { + // Copy the glyph's bitmap data into its allocated texture. + if (glyph.bitmap_data) + { + using Rml::ColorFormat; + + byte* destination = rectangle.GetTextureData(); + const byte* source = glyph.bitmap_data; + const int num_bytes_per_line = glyph.bitmap_dimensions.x * (glyph.color_format == ColorFormat::RGBA8 ? 4 : 1); + + for (int j = 0; j < glyph.bitmap_dimensions.y; ++j) + { + switch (glyph.color_format) + { + case ColorFormat::A8: + { + for (int k = 0; k < num_bytes_per_line; ++k) + destination[k * 4 + 3] = source[k]; + } + break; + case ColorFormat::RGBA8: + { + memcpy(destination, source, num_bytes_per_line); + } + break; + } + + destination += rectangle.GetTextureStride(); + source += num_bytes_per_line; + } + } + } + else + { + effect->GenerateGlyphTexture(rectangle.GetTextureData(), Vector2i(box.dimensions), rectangle.GetTextureStride(), glyph); + } + } + + return true; +} + +void FontFaceLayer::GenerateGeometry(Geometry* geometry, const FontGlyphIndex glyph_index, const Vector2f position, const Colourb colour) const +{ + auto it = character_boxes.find(glyph_index); + if (it == character_boxes.end()) + return; + + const TextureBox& box = it->second; + + if (box.texture_index < 0) + return; + + // Generate the geometry for the character. + Vector& character_vertices = geometry[box.texture_index].GetVertices(); + Vector& character_indices = geometry[box.texture_index].GetIndices(); + + character_vertices.resize(character_vertices.size() + 4); + character_indices.resize(character_indices.size() + 6); + Rml::GeometryUtilities::GenerateQuad(&character_vertices[0] + (character_vertices.size() - 4), + &character_indices[0] + (character_indices.size() - 6), Vector2f(position.x + box.origin.x, position.y + box.origin.y).Round(), + box.dimensions, colour, box.texcoords[0], box.texcoords[1], (int)character_vertices.size() - 4); +} + +const FontEffect* FontFaceLayer::GetFontEffect() const +{ + return effect.get(); +} + +const Texture* FontFaceLayer::GetTexture(int index) +{ + RMLUI_ASSERT(index >= 0); + RMLUI_ASSERT(index < GetNumTextures()); + + return &(textures[index]); +} + +int FontFaceLayer::GetNumTextures() const +{ + return (int)textures.size(); +} + +Colourb FontFaceLayer::GetColour() const +{ + return colour; +} diff --git a/Samples/basic/harfbuzzshaping/src/FontFaceLayer.h b/Samples/basic/harfbuzzshaping/src/FontFaceLayer.h new file mode 100644 index 000000000..6e8f7f1dd --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFaceLayer.h @@ -0,0 +1,123 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTFACELAYER_H +#define FONTFACELAYER_H + +#include +#include "FontGlyph.h" +#include "TextureLayout.h" + +using Rml::byte; +using Rml::Colourb; +using Rml::FontEffect; +using Rml::Geometry; +using Rml::SharedPtr; +using Rml::Texture; +using Rml::TextureLayout; +using Rml::UniquePtr; +using Rml::UnorderedMap; +using Rml::Vector; +using Rml::Vector2f; +using Rml::Vector2i; + +class FontFaceHandleHarfBuzz; + +/** + A textured layer stored as part of a font face handle. Each handle will have at least a base + layer for the standard font. Further layers can be added to allow rendering of text effects. + + Original author: Peter Curry + Modified to support HarfBuzz text shaping. + */ + +class FontFaceLayer { +public: + FontFaceLayer(const SharedPtr& _effect); + ~FontFaceLayer(); + + /// Generates or re-generates the character and texture data for the layer. + /// @param[in] handle The handle generating this layer. + /// @param[in] effect The effect to initialise the layer with. + /// @param[in] clone The layer to optionally clone geometry and texture data from. + /// @return True if the layer was generated successfully, false if not. + bool Generate(const FontFaceHandleHarfBuzz* handle, const FontFaceLayer* clone = nullptr, bool clone_glyph_origins = false); + + /// Generates the texture data for a layer (for the texture database). + /// @param[out] texture_data The pointer to be set to the generated texture data. + /// @param[out] texture_dimensions The dimensions of the texture. + /// @param[in] texture_id The index of the texture within the layer to generate. + /// @param[in] glyphs The glyphs required by the font face handle. + bool GenerateTexture(UniquePtr& texture_data, Vector2i& texture_dimensions, int texture_id, const FontGlyphMap& glyphs); + + /// Generates the geometry required to render a single character. + /// @param[out] geometry An array of geometries this layer will write to. It must be at least as big as the number of textures in this layer. + /// @param[in] glyph_index The font's glyph index of the character to generate geometry for. + /// @param[in] position The position of the baseline. + /// @param[in] colour The colour of the string. + void GenerateGeometry(Geometry* geometry, const FontGlyphIndex glyph_index, const Vector2f position, const Colourb colour) const; + + /// Returns the effect used to generate the layer. + const FontEffect* GetFontEffect() const; + + /// Returns one of the layer's textures. + const Texture* GetTexture(int index); + /// Returns the number of textures employed by this layer. + int GetNumTextures() const; + + /// Returns the layer's colour. + Colourb GetColour() const; + +private: + struct TextureBox { + TextureBox() : texture_index(-1) {} + + // The offset, in pixels, of the baseline from the start of this character's geometry. + Vector2f origin; + // The width and height, in pixels, of this character's geometry. + Vector2f dimensions; + // The texture coordinates for the character's geometry. + Vector2f texcoords[2]; + + // The texture this character renders from. + int texture_index; + }; + + using CharacterMap = UnorderedMap; + using TextureList = Vector; + + SharedPtr effect; + + TextureLayout texture_layout; + + CharacterMap character_boxes; + TextureList textures; + Colourb colour; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontFamily.cpp b/Samples/basic/harfbuzzshaping/src/FontFamily.cpp new file mode 100644 index 000000000..c2cca50ef --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFamily.cpp @@ -0,0 +1,90 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontFamily.h" +#include "FontFace.h" +#include "FontFaceHandleHarfBuzz.h" +#include + +FontFamily::FontFamily(const String& name) : name(name) {} + +FontFamily::~FontFamily() +{ + // Multiple face entries may share memory within a single font family, although only one of them owns it. Here we make sure that all the face + // destructors are run before all the memory is released. This way we don't leave any hanging references to invalidated memory. + for (FontFaceEntry& entry : font_faces) + entry.face.reset(); +} + +FontFaceHandleHarfBuzz* FontFamily::GetFaceHandle(Style::FontStyle style, Style::FontWeight weight, int size) +{ + int best_dist = INT_MAX; + FontFace* matching_face = nullptr; + for (size_t i = 0; i < font_faces.size(); i++) + { + FontFace* face = font_faces[i].face.get(); + + if (face->GetStyle() == style) + { + const int dist = Rml::Math::Absolute((int)face->GetWeight() - (int)weight); + if (dist == 0) + { + // Direct match for weight, break the loop early. + matching_face = face; + break; + } + else if (dist < best_dist) + { + // Best match so far for weight, store the face and dist. + matching_face = face; + best_dist = dist; + } + } + } + + if (!matching_face) + return nullptr; + + return matching_face->GetHandle(size, true); +} + +FontFace* FontFamily::AddFace(FontFaceHandleFreetype ft_face, Style::FontStyle style, Style::FontWeight weight, UniquePtr face_memory) +{ + auto face = Rml::MakeUnique(ft_face, style, weight); + FontFace* result = face.get(); + + font_faces.push_back(FontFaceEntry{std::move(face), std::move(face_memory)}); + + return result; +} + +void FontFamily::ReleaseFontResources() +{ + for (auto& entry : font_faces) + entry.face->ReleaseFontResources(); +} diff --git a/Samples/basic/harfbuzzshaping/src/FontFamily.h b/Samples/basic/harfbuzzshaping/src/FontFamily.h new file mode 100644 index 000000000..f1457c57f --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontFamily.h @@ -0,0 +1,86 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTFAMILY_H +#define FONTFAMILY_H + +#include "FontEngineDefault/FontTypes.h" +#include + +using Rml::byte; +using Rml::FontFaceHandleFreetype; +using Rml::String; +using Rml::UniquePtr; +using Rml::Vector; +namespace Style = Rml::Style; + +class FontFace; +class FontFaceHandleHarfBuzz; + +/** + Original author: Peter Curry + Modified to support HarfBuzz text shaping. + */ + +class FontFamily { +public: + FontFamily(const String& name); + ~FontFamily(); + + /// Returns a handle to the most appropriate font in the family, at the correct size. + /// @param[in] style The style of the desired handle. + /// @param[in] weight The weight of the desired handle. + /// @param[in] size The size of desired handle, in points. + /// @return A valid handle if a matching (or closely matching) font face was found, nullptr otherwise. + FontFaceHandleHarfBuzz* GetFaceHandle(Style::FontStyle style, Style::FontWeight weight, int size); + + /// Adds a new face to the family. + /// @param[in] ft_face The previously loaded FreeType face. + /// @param[in] style The style of the new face. + /// @param[in] weight The weight of the new face. + /// @param[in] face_memory Optionally pass ownership of the face's memory to the face itself, automatically releasing it on destruction. + /// @return True if the face was loaded successfully, false otherwise. + FontFace* AddFace(FontFaceHandleFreetype ft_face, Style::FontStyle style, Style::FontWeight weight, UniquePtr face_memory); + + /// Releases resources owned by sized font faces, including their textures and rendered glyphs. + void ReleaseFontResources(); + +protected: + String name; + + struct FontFaceEntry { + UniquePtr face; + // Only filled if we own the memory used by the face's FreeType handle. May be shared with other faces in this family. + UniquePtr face_memory; + }; + + using FontFaceList = Vector; + FontFaceList font_faces; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontGlyph.h b/Samples/basic/harfbuzzshaping/src/FontGlyph.h new file mode 100644 index 000000000..8a0cc98d2 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontGlyph.h @@ -0,0 +1,37 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTGLYPH_H +#define FONTGLYPH_H + +#include + +using FontGlyphIndex = uint32_t; +using FontGlyphMap = Rml::UnorderedMap; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FontProvider.cpp b/Samples/basic/harfbuzzshaping/src/FontProvider.cpp new file mode 100644 index 000000000..c8d41455a --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontProvider.cpp @@ -0,0 +1,233 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontProvider.h" +#include "FontEngineDefault/../ComputeProperty.h" +#include "FontEngineDefault/FreeTypeInterface.h" +#include "FontFace.h" +#include "FontFaceHandleHarfBuzz.h" +#include "FontFamily.h" +#include + +static FontProvider* g_font_provider = nullptr; + +FontProvider::FontProvider() +{ + RMLUI_ASSERT(!g_font_provider); +} + +FontProvider::~FontProvider() +{ + RMLUI_ASSERT(g_font_provider == this); +} + +bool FontProvider::Initialise() +{ + RMLUI_ASSERT(!g_font_provider); + if (!Rml::FreeType::Initialise()) + return false; + g_font_provider = new FontProvider; + return true; +} + +void FontProvider::Shutdown() +{ + RMLUI_ASSERT(g_font_provider); + delete g_font_provider; + g_font_provider = nullptr; + Rml::FreeType::Shutdown(); +} + +FontProvider& FontProvider::Get() +{ + RMLUI_ASSERT(g_font_provider); + return *g_font_provider; +} + +FontFaceHandleHarfBuzz* FontProvider::GetFontFaceHandle(const String& family, Style::FontStyle style, Style::FontWeight weight, int size) +{ + RMLUI_ASSERTMSG(family == Rml::StringUtilities::ToLower(family), "Font family name must be converted to lowercase before entering here."); + + FontFamilyMap& families = Get().font_families; + + auto it = families.find(family); + if (it == families.end()) + return nullptr; + + return it->second->GetFaceHandle(style, weight, size); +} + +void FontProvider::ReleaseFontResources() +{ + RMLUI_ASSERT(g_font_provider); + for (auto& name_family : g_font_provider->font_families) + name_family.second->ReleaseFontResources(); +} + +bool FontProvider::LoadFontFace(const String& file_name, Style::FontWeight weight) +{ + Rml::FileInterface* file_interface = Rml::GetFileInterface(); + Rml::FileHandle handle = file_interface->Open(file_name); + + if (!handle) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load font face from %s, could not open file.", file_name.c_str()); + return false; + } + + size_t length = file_interface->Length(handle); + + auto buffer_ptr = UniquePtr(new byte[length]); + byte* buffer = buffer_ptr.get(); + file_interface->Read(buffer, length, handle); + file_interface->Close(handle); + + bool result = Get().LoadFontFace(buffer, (int)length, std::move(buffer_ptr), file_name, {}, Style::FontStyle::Normal, weight); + + return result; +} + +bool FontProvider::LoadFontFace(const byte* data, int data_size, const String& font_family, Style::FontStyle style, Style::FontWeight weight) +{ + const String source = "memory"; + + bool result = Get().LoadFontFace(data, data_size, nullptr, source, font_family, style, weight); + + return result; +} + +bool FontProvider::LoadFontFace(const byte* data, int data_size, UniquePtr face_memory, const String& source, String font_family, + Style::FontStyle style, Style::FontWeight weight) +{ + using Style::FontWeight; + + Vector face_variations; + if (!Rml::FreeType::GetFaceVariations(data, data_size, face_variations)) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load font face from '%s': Invalid or unsupported font face file format.", source.c_str()); + return false; + } + + Vector load_variations; + if (face_variations.empty()) + { + load_variations.push_back(Rml::FaceVariation{Style::FontWeight::Auto, 0, 0}); + } + else + { + // Iterate through all the face variations and pick the ones to load. The list is already sorted by (weight, width). When weight is set to + // 'auto' we load all the weights of the face. However, we only want to load one width for each weight. + for (auto it = face_variations.begin(); it != face_variations.end();) + { + if (weight != FontWeight::Auto && it->weight != weight) + { + ++it; + continue; + } + + // We don't currently have any way for users to select widths, so we search for a regular (medium) value here. + constexpr int search_width = 100; + const FontWeight current_weight = it->weight; + + int best_width_distance = Rml::Math::Absolute((int)it->width - search_width); + auto it_best_width = it; + + // Search forward to find the best 'width' with the same weight. + for (++it; it != face_variations.end(); ++it) + { + if (it->weight != current_weight) + break; + + const int width_distance = Rml::Math::Absolute((int)it->width - search_width); + if (width_distance < best_width_distance) + { + best_width_distance = width_distance; + it_best_width = it; + } + } + + load_variations.push_back(*it_best_width); + } + } + + if (load_variations.empty()) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load font face from '%s': Could not locate face with weight %d.", source.c_str(), + (int)weight); + return false; + } + + for (const Rml::FaceVariation& variation : load_variations) + { + FontFaceHandleFreetype ft_face = Rml::FreeType::LoadFace(data, data_size, source, variation.named_instance_index); + if (!ft_face) + return false; + + if (font_family.empty()) + Rml::FreeType::GetFaceStyle(ft_face, &font_family, &style, nullptr); + if (weight == FontWeight::Auto) + Rml::FreeType::GetFaceStyle(ft_face, nullptr, nullptr, &weight); + + const FontWeight variation_weight = (variation.weight == FontWeight::Auto ? weight : variation.weight); + const String font_face_description = Rml::GetFontFaceDescription(font_family, style, variation_weight); + + if (!AddFace(ft_face, font_family, style, variation_weight, std::move(face_memory))) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load font face %s from '%s'.", font_face_description.c_str(), source.c_str()); + return false; + } + + Rml::Log::Message(Rml::Log::LT_INFO, "Loaded font face %s from '%s'.", font_face_description.c_str(), source.c_str()); + } + + return true; +} + +bool FontProvider::AddFace(FontFaceHandleFreetype face, const String& family, Style::FontStyle style, Style::FontWeight weight, + UniquePtr face_memory) +{ + if (family.empty() || weight == Style::FontWeight::Auto) + return false; + + String family_lower = Rml::StringUtilities::ToLower(family); + FontFamily* font_family = nullptr; + auto it = font_families.find(family_lower); + if (it != font_families.end()) + { + font_family = (FontFamily*)it->second.get(); + } + else + { + auto font_family_ptr = Rml::MakeUnique(family_lower); + font_family = font_family_ptr.get(); + font_families[family_lower] = std::move(font_family_ptr); + } + + FontFace* font_face_result = font_family->AddFace(face, style, weight, std::move(face_memory)); + return static_cast(font_face_result); +} diff --git a/Samples/basic/harfbuzzshaping/src/FontProvider.h b/Samples/basic/harfbuzzshaping/src/FontProvider.h new file mode 100644 index 000000000..d3e9a2a69 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FontProvider.h @@ -0,0 +1,96 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef FONTPROVIDER_H +#define FONTPROVIDER_H + +#include "FontEngineDefault/FontTypes.h" +#include + +using Rml::byte; +using Rml::FontFaceHandleFreetype; +using Rml::String; +using Rml::UniquePtr; +using Rml::UnorderedMap; +using Rml::Vector; +namespace Style = Rml::Style; + +class FontFace; +class FontFamily; +class FontFaceHandleHarfBuzz; + +/** + The font provider contains all font families currently in use by RmlUi. + Original author: Peter Curry + Modified to support HarfBuzz text shaping. + */ + +class FontProvider { +public: + static bool Initialise(); + static void Shutdown(); + + /// Returns a handle to a font face that can be used to position and render text. This will return the closest match + /// it can find, but in the event a font family is requested that does not exist, nullptr will be returned instead of a + /// valid handle. + /// @param[in] family The family of the desired font handle. + /// @param[in] style The style of the desired font handle. + /// @param[in] weight The weight of the desired font handle. + /// @param[in] size The size of desired handle, in points. + /// @return A valid handle if a matching (or closely matching) font face was found, nullptr otherwise. + static FontFaceHandleHarfBuzz* GetFontFaceHandle(const String& family, Style::FontStyle style, Style::FontWeight weight, int size); + + /// Adds a new font face to the database. The face's family, style and weight will be determined from the face itself. + static bool LoadFontFace(const String& file_name, Style::FontWeight weight = Style::FontWeight::Auto); + + /// Adds a new font face from memory. + static bool LoadFontFace(const byte* data, int data_size, const String& font_family, Style::FontStyle style, Style::FontWeight weight); + + /// Releases resources owned by sized font faces, including their textures and rendered glyphs. + static void ReleaseFontResources(); + +private: + FontProvider(); + ~FontProvider(); + + static FontProvider& Get(); + + bool LoadFontFace(const byte* data, int data_size, UniquePtr face_memory, const String& source, String font_family, + Style::FontStyle style, Style::FontWeight weight); + + bool AddFace(FontFaceHandleFreetype face, const String& family, Style::FontStyle style, Style::FontWeight weight, UniquePtr face_memory); + + using FontFaceList = Vector; + using FontFamilyMap = UnorderedMap>; + + FontFamilyMap font_families; + + static const String debugger_font_family_name; +}; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.cpp b/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.cpp new file mode 100644 index 000000000..7f197ac30 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.cpp @@ -0,0 +1,457 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FreeTypeInterface.h" +#include +#include +#include +#include FT_FREETYPE_H +#include FT_MULTIPLE_MASTERS_H +#include FT_TRUETYPE_TABLES_H + +namespace FreeType +{ +static bool BuildGlyph(FT_Face ft_face, const FontGlyphIndex glyph_index, FontGlyphMap& glyphs, const float bitmap_scaling_factor); +static void BuildGlyphMap(FT_Face ft_face, int size, FontGlyphMap& glyphs, const float bitmap_scaling_factor, const bool load_default_glyphs); +static void GenerateMetrics(FT_Face ft_face, FontMetrics& metrics, float bitmap_scaling_factor); +static bool SetFontSize(FT_Face ft_face, int font_size, float& out_bitmap_scaling_factor); +static void BitmapDownscale(Rml::byte* bitmap_new, const int new_width, const int new_height, const Rml::byte* bitmap_source, const int width, + const int height, + const int pitch, const Rml::ColorFormat color_format); + +bool InitialiseFaceHandle(FontFaceHandleFreetype face, int font_size, FontGlyphMap& glyphs, FontMetrics& metrics, bool load_default_glyphs) +{ + FT_Face ft_face = (FT_Face)face; + + metrics.size = font_size; + + float bitmap_scaling_factor = 1.0f; + if (!SetFontSize(ft_face, font_size, bitmap_scaling_factor)) + return false; + + // Construct the initial list of glyphs. + BuildGlyphMap(ft_face, font_size, glyphs, bitmap_scaling_factor, load_default_glyphs); + + // Generate the metrics for the handle. + GenerateMetrics(ft_face, metrics, bitmap_scaling_factor); + + return true; +} + +bool AppendGlyph(FontFaceHandleFreetype face, int font_size, FontGlyphIndex glyph_index, FontGlyphMap& glyphs) +{ + FT_Face ft_face = (FT_Face)face; + + RMLUI_ASSERT(glyphs.find(glyph_index) == glyphs.end()); + RMLUI_ASSERT(ft_face); + + // Set face size again in case it was used at another size in another font face handle. + float bitmap_scaling_factor = 1.0f; + if (!SetFontSize(ft_face, font_size, bitmap_scaling_factor)) + return false; + + if (!BuildGlyph(ft_face, glyph_index, glyphs, bitmap_scaling_factor)) + return false; + + return true; +} + +int GetKerning(FontFaceHandleFreetype face, int font_size, FontGlyphIndex lhs, FontGlyphIndex rhs) +{ + FT_Face ft_face = (FT_Face)face; + + RMLUI_ASSERT(FT_HAS_KERNING(ft_face)); + + // Set face size again in case it was used at another size in another font face handle. + // Font size value of zero assumes it is already set. + if (font_size > 0) + { + float bitmap_scaling_factor = 1.0f; + if (!SetFontSize(ft_face, font_size, bitmap_scaling_factor) || bitmap_scaling_factor != 1.0f) + return 0; + } + + FT_Vector ft_kerning; + + FT_Error ft_error = FT_Get_Kerning(ft_face, lhs, rhs,FT_KERNING_DEFAULT, &ft_kerning); + + if (ft_error) + return 0; + + int kerning = ft_kerning.x >> 6; + return kerning; +} + +FontGlyphIndex GetGlyphIndexFromCharacter(FontFaceHandleFreetype face, Character character) +{ + return FT_Get_Char_Index((FT_Face)face, (FT_ULong)character); +} + +static bool BuildGlyph(FT_Face ft_face, const FontGlyphIndex glyph_index, FontGlyphMap& glyphs, + const float bitmap_scaling_factor) +{ + if (glyph_index == 0) + return false; + + FT_Error error = FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_COLOR); + if (error != 0) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unable to load glyph at index '%u' on the font face '%s %s'; error code: %d.", + (unsigned int)glyph_index, + ft_face->family_name, ft_face->style_name, error); + return false; + } + + error = FT_Render_Glyph(ft_face->glyph, FT_RENDER_MODE_NORMAL); + if (error != 0) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unable to render glyph at index '%u' on the font face '%s %s'; error code: %d.", + (unsigned int)glyph_index, + ft_face->family_name, ft_face->style_name, error); + return false; + } + + auto result = glyphs.emplace(glyph_index, Rml::FontGlyph{}); + if (!result.second) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Glyph index '%u' is already loaded in the font face '%s %s'.", (unsigned int)glyph_index, + ft_face->family_name, ft_face->style_name); + return false; + } + + Rml::FontGlyph& glyph = result.first->second; + + FT_GlyphSlot ft_glyph = ft_face->glyph; + + // Set the glyph's dimensions. + glyph.dimensions.x = ft_glyph->metrics.width >> 6; + glyph.dimensions.y = ft_glyph->metrics.height >> 6; + + // Set the glyph's bearing. + glyph.bearing.x = ft_glyph->metrics.horiBearingX >> 6; + glyph.bearing.y = ft_glyph->metrics.horiBearingY >> 6; + + // Set the glyph's advance. + glyph.advance = ft_glyph->metrics.horiAdvance >> 6; + + // Set the glyph's bitmap dimensions. + glyph.bitmap_dimensions.x = ft_glyph->bitmap.width; + glyph.bitmap_dimensions.y = ft_glyph->bitmap.rows; + + // Determine new metrics if we need to scale the bitmap received from FreeType. Only allow bitmap downscaling. + const bool scale_bitmap = (bitmap_scaling_factor < 1.f); + if (scale_bitmap) + { + glyph.dimensions = Rml::Vector2i(Rml::Vector2f(glyph.dimensions) * bitmap_scaling_factor); + glyph.bearing = Rml::Vector2i(Rml::Vector2f(glyph.bearing) * bitmap_scaling_factor); + glyph.advance = int(float(glyph.advance) * bitmap_scaling_factor); + glyph.bitmap_dimensions = Rml::Vector2i(Rml::Vector2f(glyph.bitmap_dimensions) * bitmap_scaling_factor); + } + + // Copy the glyph's bitmap data from the FreeType glyph handle to our glyph handle. + if (glyph.bitmap_dimensions.x * glyph.bitmap_dimensions.y != 0) + { + // Check if the pixel mode is supported. + if (ft_glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO && ft_glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY && + ft_glyph->bitmap.pixel_mode != FT_PIXEL_MODE_BGRA) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unable to render glyph on the font face '%s %s': unsupported pixel mode (%d).", + ft_glyph->face->family_name, ft_glyph->face->style_name, ft_glyph->bitmap.pixel_mode); + } + else if (ft_glyph->bitmap.pixel_mode == FT_PIXEL_MODE_MONO && scale_bitmap) + { + Rml::Log::Message(Rml::Log::LT_WARNING, "Unable to render glyph on the font face '%s %s': bitmap scaling unsupported in mono pixel mode.", + ft_glyph->face->family_name, ft_glyph->face->style_name); + } + else + { + const int num_bytes_per_pixel = (ft_glyph->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA ? 4 : 1); + glyph.color_format = (ft_glyph->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA ? Rml::ColorFormat::RGBA8 : Rml::ColorFormat::A8); + + glyph.bitmap_owned_data.reset(new Rml::byte[glyph.bitmap_dimensions.x * glyph.bitmap_dimensions.y * num_bytes_per_pixel]); + glyph.bitmap_data = glyph.bitmap_owned_data.get(); + Rml::byte* destination_bitmap = glyph.bitmap_owned_data.get(); + const Rml::byte* source_bitmap = ft_glyph->bitmap.buffer; + + // Copy the bitmap data into the newly-allocated space on our glyph. + switch (ft_glyph->bitmap.pixel_mode) + { + case FT_PIXEL_MODE_MONO: + { + // Unpack 1-bit data into 8-bit. + for (int i = 0; i < glyph.bitmap_dimensions.y; ++i) + { + int mask = 0x80; + const Rml::byte* source_byte = source_bitmap; + for (int j = 0; j < glyph.bitmap_dimensions.x; ++j) + { + if ((*source_byte & mask) == mask) + destination_bitmap[j] = 255; + else + destination_bitmap[j] = 0; + + mask >>= 1; + if (mask <= 0) + { + mask = 0x80; + ++source_byte; + } + } + + destination_bitmap += glyph.bitmap_dimensions.x; + source_bitmap += ft_glyph->bitmap.pitch; + } + } + break; + case FT_PIXEL_MODE_GRAY: + case FT_PIXEL_MODE_BGRA: + { + if (scale_bitmap) + { + // Resize the glyph data to the new dimensions. + BitmapDownscale(destination_bitmap, glyph.bitmap_dimensions.x, glyph.bitmap_dimensions.y, source_bitmap, + (int)ft_glyph->bitmap.width, (int)ft_glyph->bitmap.rows, ft_glyph->bitmap.pitch, glyph.color_format); + } + else + { + // Copy the glyph data directly. + const int num_bytes_per_line = glyph.bitmap_dimensions.x * num_bytes_per_pixel; + for (int i = 0; i < glyph.bitmap_dimensions.y; ++i) + { + memcpy(destination_bitmap, source_bitmap, num_bytes_per_line); + destination_bitmap += num_bytes_per_line; + source_bitmap += ft_glyph->bitmap.pitch; + } + } + + if (glyph.color_format == Rml::ColorFormat::RGBA8) + { + // Swizzle channels (BGRA -> RGBA) and un-premultiply alpha. + destination_bitmap = glyph.bitmap_owned_data.get(); + + for (int k = 0; k < glyph.bitmap_dimensions.x * glyph.bitmap_dimensions.y * num_bytes_per_pixel; k += 4) + { + Rml::byte b = destination_bitmap[k]; + Rml::byte g = destination_bitmap[k + 1]; + Rml::byte r = destination_bitmap[k + 2]; + const Rml::byte alpha = destination_bitmap[k + 3]; + RMLUI_ASSERTMSG(b <= alpha && g <= alpha && r <= alpha, "Assumption of glyph data being premultiplied is broken."); + + if (alpha > 0 && alpha < 255) + { + b = Rml::byte((b * 255) / alpha); + g = Rml::byte((g * 255) / alpha); + r = Rml::byte((r * 255) / alpha); + } + + destination_bitmap[k] = r; + destination_bitmap[k + 1] = g; + destination_bitmap[k + 2] = b; + destination_bitmap[k + 3] = alpha; + } + } + } + break; + } + } + } + + return true; +} + +static void BuildGlyphMap(FT_Face ft_face, int size, FontGlyphMap& glyphs, const float bitmap_scaling_factor, const bool load_default_glyphs) +{ + if (load_default_glyphs) + { + glyphs.reserve(128); + + // Add the ASCII characters now. Other characters are added later as needed. + FT_ULong code_min = 32; + FT_ULong code_max = 126; + + for (FT_ULong character_code = code_min; character_code <= code_max; ++character_code) + { + FT_UInt index = FT_Get_Char_Index(ft_face, character_code); + BuildGlyph(ft_face, index, glyphs, bitmap_scaling_factor); + } + } + + // Add a replacement character for rendering unknown characters. + FontGlyphIndex replacement_glyph_index = FT_Get_Char_Index(ft_face, (FT_ULong)Rml::Character::Replacement); + auto it = glyphs.find(replacement_glyph_index); + if (it == glyphs.end()) + { + Rml::FontGlyph glyph; + glyph.dimensions = {size / 3, (size * 2) / 3}; + glyph.bitmap_dimensions = glyph.dimensions; + glyph.advance = glyph.dimensions.x + 2; + glyph.bearing = {1, glyph.dimensions.y}; + + glyph.bitmap_owned_data.reset(new Rml::byte[glyph.bitmap_dimensions.x * glyph.bitmap_dimensions.y]); + glyph.bitmap_data = glyph.bitmap_owned_data.get(); + + for (int y = 0; y < glyph.bitmap_dimensions.y; y++) + { + for (int x = 0; x < glyph.bitmap_dimensions.x; x++) + { + constexpr int stroke = 1; + int i = y * glyph.bitmap_dimensions.x + x; + bool near_edge = (x < stroke || x >= glyph.bitmap_dimensions.x - stroke || y < stroke || y >= glyph.bitmap_dimensions.y - stroke); + glyph.bitmap_owned_data[i] = (near_edge ? 0xdd : 0); + } + } + + glyphs[replacement_glyph_index] = std::move(glyph); + } +} + +static void GenerateMetrics(FT_Face ft_face, FontMetrics& metrics, float bitmap_scaling_factor) +{ + metrics.ascent = ft_face->size->metrics.ascender * bitmap_scaling_factor / float(1 << 6); + metrics.descent = -ft_face->size->metrics.descender * bitmap_scaling_factor / float(1 << 6); + metrics.line_spacing = ft_face->size->metrics.height * bitmap_scaling_factor / float(1 << 6); + + metrics.underline_position = FT_MulFix(-ft_face->underline_position, ft_face->size->metrics.y_scale) * bitmap_scaling_factor / float(1 << 6); + metrics.underline_thickness = FT_MulFix(ft_face->underline_thickness, ft_face->size->metrics.y_scale) * bitmap_scaling_factor / float(1 << 6); + metrics.underline_thickness = Rml::Math::Max(metrics.underline_thickness, 1.0f); + + // Determine the x-height of this font face. + FT_UInt index = FT_Get_Char_Index(ft_face, 'x'); + if (index != 0 && FT_Load_Glyph(ft_face, index, 0) == 0) + metrics.x_height = ft_face->glyph->metrics.height * bitmap_scaling_factor / float(1 << 6); + else + metrics.x_height = 0.5f * metrics.line_spacing; +} + +static bool SetFontSize(FT_Face ft_face, int font_size, float& out_bitmap_scaling_factor) +{ + RMLUI_ASSERT(out_bitmap_scaling_factor == 1.f); + + FT_Error error = 0; + + // Set the character size on the font face. + error = FT_Set_Char_Size(ft_face, 0, font_size << 6, 0, 0); + + // If setting char size fails, try to select a bitmap strike instead when available. + if (error != 0 && FT_HAS_FIXED_SIZES(ft_face)) + { + constexpr int a_big_number = INT_MAX / 2; + int heuristic_min = INT_MAX; + int index_min = -1; + + // Select the bitmap strike with the smallest size *above* font_size, or else the largest size. + for (int i = 0; i < ft_face->num_fixed_sizes; i++) + { + const int size_diff = ft_face->available_sizes[i].height - font_size; + const int heuristic = (size_diff < 0 ? a_big_number - size_diff : size_diff); + + if (heuristic < heuristic_min) + { + index_min = i; + heuristic_min = heuristic; + } + } + + if (index_min >= 0) + { + out_bitmap_scaling_factor = float(font_size) / ft_face->available_sizes[index_min].height; + + // Add some tolerance to the scaling factor to avoid unnecessary scaling. Only allow downscaling. + constexpr float bitmap_scaling_factor_threshold = 0.95f; + if (out_bitmap_scaling_factor >= bitmap_scaling_factor_threshold) + out_bitmap_scaling_factor = 1.f; + + error = FT_Select_Size(ft_face, index_min); + } + } + + if (error != 0) + { + Rml::Log::Message(Rml::Log::LT_ERROR, "Unable to set the character size '%d' on the font face '%s %s'.", font_size, ft_face->family_name, + ft_face->style_name); + return false; + } + + return true; +} + +static void BitmapDownscale(Rml::byte* bitmap_new, const int new_width, const int new_height, const Rml::byte* bitmap_source, const int width, + const int height, const int pitch, const Rml::ColorFormat color_format) +{ + // Average filter for downscaling bitmap images, based on https://stackoverflow.com/a/9571580 + constexpr int max_num_channels = 4; + const int num_channels = (color_format == Rml::ColorFormat::RGBA8 ? 4 : 1); + + const float xscale = float(new_width) / width; + const float yscale = float(new_height) / height; + const float sumscale = xscale * yscale; + + float yend = 0.0f; + for (int f = 0; f < new_height; f++) // y on output + { + const float ystart = yend; + yend = (f + 1) * (1.f / yscale); + if (yend >= height) + yend = height - 0.001f; + + float xend = 0.0; + for (int g = 0; g < new_width; g++) // x on output + { + float xstart = xend; + xend = (g + 1) * (1.f / xscale); + if (xend >= width) + xend = width - 0.001f; + + float sum[max_num_channels] = {}; + for (int y = (int)ystart; y <= (int)yend; ++y) + { + float yportion = 1.0f; + if (y == (int)ystart) + yportion -= ystart - y; + if (y == (int)yend) + yportion -= y + 1 - yend; + + for (int x = (int)xstart; x <= (int)xend; ++x) + { + float xportion = 1.0f; + if (x == (int)xstart) + xportion -= xstart - x; + if (x == (int)xend) + xportion -= x + 1 - xend; + + for (int i = 0; i < num_channels; i++) + sum[i] += bitmap_source[y * pitch + x * num_channels + i] * yportion * xportion; + } + } + + for (int i = 0; i < num_channels; i++) + bitmap_new[(f * new_width + g) * num_channels + i] = (Rml::byte)Rml::Math::Min(sum[i] * sumscale, 255.f); + } + } +} +} // namespace FreeType diff --git a/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.h b/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.h new file mode 100644 index 000000000..0dd5d3f1e --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/FreeTypeInterface.h @@ -0,0 +1,52 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontEngineDefault/FontTypes.h" +#include "FontGlyph.h" +#include + +using Rml::Character; +using Rml::FontFaceHandleFreetype; +using Rml::FontMetrics; + +namespace FreeType { + +// Initializes a face for a given font size. Glyphs are filled with the ASCII subset, and the font face metrics are set. +bool InitialiseFaceHandle(FontFaceHandleFreetype face, int font_size, FontGlyphMap& glyphs, FontMetrics& metrics, bool load_default_glyphs); + +// Build a new glyph representing the given glyph index and append to 'glyphs'. +bool AppendGlyph(FontFaceHandleFreetype face, int font_size, FontGlyphIndex glyph_index, FontGlyphMap& glyphs); + +// Returns the kerning between two characters given by glyph indices. +// 'font_size' value of zero assumes the font size is already set on the face, and skips this step for performance reasons. +int GetKerning(FontFaceHandleFreetype face, int font_size, FontGlyphIndex lhs, FontGlyphIndex rhs); + +// Returns the corresponding glyph index from a character code. +FontGlyphIndex GetGlyphIndexFromCharacter(FontFaceHandleFreetype face, Character character); + +} // namespace FreeType diff --git a/Samples/basic/harfbuzzshaping/src/LanguageData.h b/Samples/basic/harfbuzzshaping/src/LanguageData.h new file mode 100644 index 000000000..f2ac73a47 --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/LanguageData.h @@ -0,0 +1,46 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef LANGUAGEDATA_H +#define LANGUAGEDATA_H + +#include + +enum class TextFlowDirection { + LeftToRight, + RightToLeft, +}; + +struct LanguageData { + Rml::String script_code; + TextFlowDirection text_flow_direction; +}; + +using LanguageDataMap = Rml::UnorderedMap; + +#endif diff --git a/Samples/basic/harfbuzzshaping/src/main.cpp b/Samples/basic/harfbuzzshaping/src/main.cpp new file mode 100644 index 000000000..c9803685c --- /dev/null +++ b/Samples/basic/harfbuzzshaping/src/main.cpp @@ -0,0 +1,186 @@ +/* + * This source file is part of RmlUi, the HTML/CSS Interface Middleware + * + * For the latest information, see http://github.com/mikke89/RmlUi + * + * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd + * Copyright (c) 2019-2023 The RmlUi Team, and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "FontEngineInterfaceHarfBuzz.h" +#include +#include +#include +#include + +/* + This demo shows how to create a custom text-shaping font engine implementation using HarfBuzz. +*/ + +// Toggle this variable to enable/disable text shaping. +constexpr bool EnableTextShaping = true; + +class HarfBuzzEventListener : public Rml::EventListener { +public: + HarfBuzzEventListener(const Rml::String& value, Rml::ElementDocument* document) : + value(value), english_element(document->GetElementById("lorem-ipsum-en")), arabic_element(document->GetElementById("lorem-ipsum-ar")), + hindi_element(document->GetElementById("lorem-ipsum-hi")) + {} + + void ProcessEvent(Rml::Event& /*event*/) override + { + if (value == "set-english") + { + english_element->SetClass("hide-lorem-ipsum", false); + arabic_element->SetClass("hide-lorem-ipsum", true); + hindi_element->SetClass("hide-lorem-ipsum", true); + } + else if (value == "set-arabic") + { + english_element->SetClass("hide-lorem-ipsum", true); + arabic_element->SetClass("hide-lorem-ipsum", false); + hindi_element->SetClass("hide-lorem-ipsum", true); + } + else if (value == "set-hindi") + { + english_element->SetClass("hide-lorem-ipsum", true); + arabic_element->SetClass("hide-lorem-ipsum", true); + hindi_element->SetClass("hide-lorem-ipsum", false); + } + } + + void OnDetach(Rml::Element* /*element*/) override { delete this; } + +private: + Rml::String value; + + Rml::Element* english_element; + Rml::Element* arabic_element; + Rml::Element* hindi_element; +}; + +#if defined RMLUI_PLATFORM_WIN32 + #include +int APIENTRY WinMain(HINSTANCE /*instance_handle*/, HINSTANCE /*previous_instance_handle*/, char* /*command_line*/, int /*command_show*/) +#else +int main(int /*argc*/, char** /*argv*/) +#endif +{ + int window_width = 1024; + int window_height = 768; + + // Initializes the shell which provides common functionality used by the included samples. + if (!Shell::Initialize()) + return -1; + + // Constructs the system and render interfaces, creates a window, and attaches the renderer. + if (!Backend::Initialize("HarfBuzz Text Shaping Sample", window_width, window_height, true)) + { + Shell::Shutdown(); + return -1; + } + + // Install the custom interfaces constructed by the backend before initializing RmlUi. + Rml::SetSystemInterface(Backend::GetSystemInterface()); + Rml::SetRenderInterface(Backend::GetRenderInterface()); + + // Construct and load the font interface. + Rml::UniquePtr font_interface = nullptr; + if (EnableTextShaping) + { + font_interface = Rml::MakeUnique(); + Rml::SetFontEngineInterface(font_interface.get()); + + // Add language data to the font engine's internal language lookup table. + font_interface->RegisterLanguage("en", "Latn", TextFlowDirection::LeftToRight); + font_interface->RegisterLanguage("ar", "Arab", TextFlowDirection::RightToLeft); + font_interface->RegisterLanguage("hi", "Deva", TextFlowDirection::LeftToRight); + } + + // RmlUi initialisation. + Rml::Initialise(); + + // Create the main RmlUi context. + Rml::Context* context = Rml::CreateContext("main", Rml::Vector2i(window_width, window_height)); + if (!context) + { + Rml::Shutdown(); + Backend::Shutdown(); + Shell::Shutdown(); + return -1; + } + + Rml::Debugger::Initialise(context); + + // Load required fonts. + Rml::String font_paths[3] = { + "assets/LatoLatin-Regular.ttf", + "basic/harfbuzzshaping/data/Cairo-Regular.ttf", + "basic/harfbuzzshaping/data/Poppins-Regular.ttf", + }; + for (const Rml::String& font_path : font_paths) + if (!Rml::LoadFontFace(font_path)) + { + Rml::Shutdown(); + Backend::Shutdown(); + Shell::Shutdown(); + return -1; + } + + // Load and show the demo document. + if (Rml::ElementDocument* document = context->LoadDocument("basic/harfbuzzshaping/data/harfbuzzshaping.rml")) + { + if (auto el = document->GetElementById("title")) + el->SetInnerRML("HarfBuzz Text Shaping"); + + document->Show(); + + // Create event handlers. + for (const Rml::String& button_id : {"set-english", "set-arabic", "set-hindi"}) + document->GetElementById(button_id)->AddEventListener(Rml::EventId::Click, new HarfBuzzEventListener(button_id, document)); + } + + bool running = true; + while (running) + { + running = Backend::ProcessEvents(context, &Shell::ProcessKeyDownShortcuts, true); + + context->Update(); + + Backend::BeginFrame(); + context->Render(); + Backend::PresentFrame(); + } + + // Shut down debugger before font interface. + Rml::Debugger::Shutdown(); + if (EnableTextShaping) + font_interface.reset(); + + // Shutdown RmlUi. + Rml::Shutdown(); + + Backend::Shutdown(); + Shell::Shutdown(); + + return 0; +} diff --git a/Samples/readme.md b/Samples/readme.md index 6c5dcbf6e..40a060a85 100644 --- a/Samples/readme.md +++ b/Samples/readme.md @@ -19,6 +19,7 @@ This directory contains basic applications that demonstrate initialisation, usag - `databinding` setting up and using data bindings - `demo` demonstrates a variety of features in RmlUi and includes a sandbox for playing with RML/RCSS - `drag` dragging elements between containers +- `harfbuzzshaping` advanced text shaping, only enabled when [HarfBuzz](https://harfbuzz.github.io/) is enabled - `loaddocument` loading your first document - `lottie` playing Lottie animations, only enabled with the [Lottie plugin](https://mikke89.github.io/RmlUiDoc/pages/cpp_manual/lottie.html) - `svg` render SVG images, only enabled with the [SVG plugin](https://mikke89.github.io/RmlUiDoc/pages/cpp_manual/svg.html) diff --git a/readme.md b/readme.md index 476ed37d6..8d589d72c 100644 --- a/readme.md +++ b/readme.md @@ -455,6 +455,7 @@ See [Source/Debugger/LICENSE.txt](Source/Debugger/LICENSE.txt) - SIL Open Font L See - [Samples/assets/LICENSE.txt](Samples/assets/LICENSE.txt) - [Samples/basic/bitmapfont/data/LICENSE.txt](Samples/basic/bitmapfont/data/LICENSE.txt) +- [Samples/basic/harfbuzz/data/LICENSE.txt](Samples/basic/harfbuzz/data/LICENSE.txt) - [Samples/basic/lottie/data/LICENSE.txt](Samples/basic/lottie/data/LICENSE.txt) - [Samples/basic/svg/data/LICENSE.txt](Samples/basic/svg/data/LICENSE.txt)