Feature options on Fonts Adds runtime and editor support for setting feature flags on a Font. The biggest change to the font engine is that the feature options are now stored on the Font object itself instead of hard-coded during shaping. This is nice as it requires no extra data to be piped through for individual run styling. It also means that we generalized the concept of creating a variable font as configuring a version of the font (see withOptions replacing makeVariation) so that variable axis and feature settings are treated as options to a Font configuration. This also allows us to track existing variations and options on the configured Font such that any further call to "withOptions" on that already configured Font will propagate previous changes if not overridden. This fortuitously also fixes an issue the modifiers were exhibiting where a variation set on the TextStyle that wasn't part of the modifier set would be lost. Diffs= 31d9a5424 Feature options on Fonts (#5479) Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head index ae47926..c5fb219 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -8b7587241233b7b884c5fa2976ed0cecf45c8022 +31d9a54243e694ede76dc133cf209d7b67c13708
diff --git a/dev/setup_premake.sh b/dev/setup_premake.sh new file mode 100644 index 0000000..220c650 --- /dev/null +++ b/dev/setup_premake.sh
@@ -0,0 +1,34 @@ +#!/bin/bash + +unameOut="$(uname -s)" +case "${unameOut}" in +Linux*) MACHINE=linux ;; +Darwin*) MACHINE=mac ;; +CYGWIN*) MACHINE=cygwin ;; +MINGW*) MACHINE=mingw ;; +*) MACHINE="UNKNOWN:${unameOut}" ;; +esac + +# check if use has already installed premake5 +if ! command -v premake5 &>/dev/null; then + # no premake found in path + if [[ ! -f "bin/premake5" ]]; then + mkdir -p bin + pushd bin + echo Downloading Premake5 + if [ "$MACHINE" = 'mac' ]; then + PREMAKE_URL=https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-macosx.tar.gz + else + PREMAKE_URL=https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz + fi + curl $PREMAKE_URL -L -o premake.tar.gz + # Export premake5 into bin + tar -xvf premake.tar.gz 2>/dev/null + # Delete downloaded archive + rm premake.tar.gz + popd + fi + export PREMAKE=$PWD/bin/premake5 +else + export PREMAKE=premake5 +fi
diff --git a/dev/test.sh b/dev/test.sh index 2707558..b11c94b 100755 --- a/dev/test.sh +++ b/dev/test.sh
@@ -3,6 +3,8 @@ set -e source ../dependencies/config_directories.sh +source setup_premake.sh + pushd test &>/dev/null OPTION=$1 @@ -14,7 +16,7 @@ exit elif [ "$OPTION" = "clean" ]; then echo Cleaning project ... - premake5 --scripts=../../build clean || exit 1 + $PREMAKE --scripts=../../build clean || exit 1 shift elif [ "$OPTION" = "memory" ]; then echo Will perform memory checks... @@ -26,7 +28,7 @@ shift fi -premake5 --scripts=../../build gmake2 || exit 1 +$PREMAKE --scripts=../../build gmake2 || exit 1 make -j7 || exit 1 for file in ./build/bin/debug/*; do
diff --git a/include/rive/text/font_hb.hpp b/include/rive/text/font_hb.hpp index 61b627b..23b7a11 100644 --- a/include/rive/text/font_hb.hpp +++ b/include/rive/text/font_hb.hpp
@@ -7,17 +7,13 @@ #include "rive/factory.hpp" #include "rive/text_engine.hpp" +#include "hb.h" -struct hb_font_t; -struct hb_draw_funcs_t; +#include <unordered_map> class HBFont : public rive::Font { - hb_draw_funcs_t* m_DrawFuncs; - public: - hb_font_t* m_Font; - // We assume ownership of font! HBFont(hb_font_t* font); ~HBFont() override; @@ -25,15 +21,25 @@ Axis getAxis(uint16_t index) const override; uint16_t getAxisCount() const override; float getAxisValue(uint32_t axisTag) const override; - std::vector<Coord> getCoords() const override; - rive::rcp<rive::Font> makeAtCoords(rive::Span<const Coord>) const override; + uint32_t getFeatureValue(uint32_t featureTag) const override; + rive::RawPath getPath(rive::GlyphID) const override; rive::SimpleArray<rive::Paragraph> onShapeText(rive::Span<const rive::Unichar>, rive::Span<const rive::TextRun>) const override; + rive::SimpleArray<uint32_t> features() const override; + rive::rcp<Font> withOptions(rive::Span<const Coord> variableAxes, + rive::Span<const Feature> features) const override; bool hasGlyph(rive::Span<const rive::Unichar>); static rive::rcp<rive::Font> Decode(rive::Span<const uint8_t>); + hb_font_t* font() const { return m_font; } + +private: + HBFont(hb_font_t* font, + std::unordered_map<hb_tag_t, float> axisValues, + std::unordered_map<hb_tag_t, uint32_t> featureValues, + std::vector<hb_feature_t> features); // If the platform can supply fallback font(s), set this function pointer. // It will be called with a span of unichars, and the platform attempts to @@ -42,7 +48,22 @@ using FallbackProc = rive::rcp<rive::Font> (*)(rive::Span<const rive::Unichar>); +public: static FallbackProc gFallbackProc; + + hb_font_t* m_font; + + // The features list to pass directly to Harfbuzz. + std::vector<hb_feature_t> m_features; + +private: + hb_draw_funcs_t* m_drawFuncs; + + // Feature value lookup based on tag. + std::unordered_map<hb_tag_t, uint32_t> m_featureValues; + + // Axis value lookup based on for the feature. + std::unordered_map<hb_tag_t, float> m_axisValues; }; #endif
diff --git a/include/rive/text_engine.hpp b/include/rive/text_engine.hpp index 8e4f257..881be9b 100644 --- a/include/rive/text_engine.hpp +++ b/include/rive/text_engine.hpp
@@ -99,10 +99,7 @@ const LineMetrics& lineMetrics() const { return m_LineMetrics; } - // This is experimental - // -- may only be needed by Editor - // -- so it may be removed from here later - // + // Variable axis available for the font. struct Axis { uint32_t tag; @@ -111,33 +108,48 @@ float max; }; - // Returns the canonical set of Axes for this font. Use this to know - // what variations are possible. If you want to know the specific - // coordinate within that variations space for *this* font, call - // getCoords(). - // - std::vector<Axis> getAxes() const; - virtual Axis getAxis(uint16_t index) const = 0; - virtual uint16_t getAxisCount() const = 0; - + // Variable axis setting. struct Coord { uint32_t axis; float value; }; - // Returns the specific coords in variation space for this font. - // If you want to have a description of the entire variation space, - // call getAxes(). - // - virtual std::vector<Coord> getCoords() const = 0; + // Returns the count of variable axes available for this font. + virtual uint16_t getAxisCount() const = 0; + // Returns the definition of the Axis at the provided index. + virtual Axis getAxis(uint16_t index) const = 0; + + // Value for the axis, if a Coord has been provided the value from the Coord + // will be used. Otherwise the default value for the axis will be returned. virtual float getAxisValue(uint32_t axisTag) const = 0; - virtual rcp<Font> makeAtCoords(Span<const Coord>) const = 0; + // Font feature. + struct Feature + { + uint32_t tag; + uint32_t value; + }; + + // Returns the features available for this font. + virtual SimpleArray<uint32_t> features() const = 0; + + // Value for the feature, if no value has been provided a (uint32_t)-1 is + // returned to signal that the text engine will pick the best feature value + // for the content. + virtual uint32_t getFeatureValue(uint32_t featureTag) const = 0; + + rcp<Font> makeAtCoords(Span<const Coord> coords) const + { + return withOptions(coords, Span<const Feature>(nullptr, 0)); + } rcp<Font> makeAtCoord(Coord c) { return this->makeAtCoords(Span<const Coord>(&c, 1)); } + virtual rcp<Font> withOptions(Span<const Coord> variableAxes, + Span<const Feature> features) const = 0; + // Returns a 1-point path for this glyph. It will be positioned // relative to (0,0) with the typographic baseline at y = 0. //
diff --git a/src/text/font_hb.cpp b/src/text/font_hb.cpp index 2bd8eae..9dec0fd 100644 --- a/src/text/font_hb.cpp +++ b/src/text/font_hb.cpp
@@ -12,6 +12,7 @@ #include "hb.h" #include "hb-ot.h" +#include <unordered_set> extern "C" { @@ -118,27 +119,112 @@ return {-extents.ascender * gInvScale, -extents.descender * gInvScale}; } -HBFont::HBFont(hb_font_t* font) : - Font(make_lmx(font)), m_Font(font) // we just take ownership, no need to call reference() +HBFont::HBFont(hb_font_t* font) : HBFont(font, {}, {}, {}) {} + +HBFont::HBFont(hb_font_t* font, + std::unordered_map<hb_tag_t, float> axisValues, + std::unordered_map<hb_tag_t, uint32_t> featureValues, + std::vector<hb_feature_t> features) : + Font(make_lmx(font)), + m_font(font), + m_features(features), + m_featureValues(featureValues), + m_axisValues(axisValues) { - m_DrawFuncs = hb_draw_funcs_create(); - hb_draw_funcs_set_move_to_func(m_DrawFuncs, rpath_move_to, nullptr, nullptr); - hb_draw_funcs_set_line_to_func(m_DrawFuncs, rpath_line_to, nullptr, nullptr); - hb_draw_funcs_set_quadratic_to_func(m_DrawFuncs, rpath_quad_to, nullptr, nullptr); - hb_draw_funcs_set_cubic_to_func(m_DrawFuncs, rpath_cubic_to, nullptr, nullptr); - hb_draw_funcs_set_close_path_func(m_DrawFuncs, rpath_close, nullptr, nullptr); - hb_draw_funcs_make_immutable(m_DrawFuncs); + m_drawFuncs = hb_draw_funcs_create(); + hb_draw_funcs_set_move_to_func(m_drawFuncs, rpath_move_to, nullptr, nullptr); + hb_draw_funcs_set_line_to_func(m_drawFuncs, rpath_line_to, nullptr, nullptr); + hb_draw_funcs_set_quadratic_to_func(m_drawFuncs, rpath_quad_to, nullptr, nullptr); + hb_draw_funcs_set_cubic_to_func(m_drawFuncs, rpath_cubic_to, nullptr, nullptr); + hb_draw_funcs_set_close_path_func(m_drawFuncs, rpath_close, nullptr, nullptr); + hb_draw_funcs_make_immutable(m_drawFuncs); } HBFont::~HBFont() { - hb_draw_funcs_destroy(m_DrawFuncs); - hb_font_destroy(m_Font); + hb_draw_funcs_destroy(m_drawFuncs); + hb_font_destroy(m_font); +} + +static void fillLanguageFeatures(hb_face_t* face, + hb_tag_t tag, + uint32_t scriptIndex, + uint32_t languageIndex, + std::unordered_set<uint32_t>& features) +{ + auto featureCount = hb_ot_layout_language_get_feature_tags(face, + tag, + scriptIndex, + languageIndex, + 0, + nullptr, + nullptr); + auto featureTags = std::vector<hb_tag_t>(featureCount); + hb_ot_layout_language_get_feature_tags(face, + tag, + scriptIndex, + languageIndex, + 0, + &featureCount, + featureTags.data()); + + for (auto featureTag : featureTags) + { + features.emplace(featureTag); + } +} + +static void fillFeatures(hb_face_t* face, hb_tag_t tag, std::unordered_set<uint32_t>& features) +{ + auto scriptCount = hb_ot_layout_table_get_script_tags(face, tag, 0, nullptr, nullptr); + auto scripts = std::vector<hb_tag_t>(scriptCount); + hb_ot_layout_table_get_script_tags(face, tag, 0, &scriptCount, scripts.data()); + for (uint32_t i = 0; i < scriptCount; ++i) + { + auto languageCount = + hb_ot_layout_script_get_language_tags(face, tag, i, 0, nullptr, nullptr); + + if (languageCount > 0) + { + auto languages = std::vector<hb_tag_t>(languageCount); + hb_ot_layout_script_get_language_tags(face, + tag, + i, + 0, + &languageCount, + languages.data()); + + for (uint32_t j = 0; j < languageCount; ++j) + { + fillLanguageFeatures(face, tag, i, j, features); + } + } + else + { + fillLanguageFeatures(face, tag, i, HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, features); + } + } +} + +rive::SimpleArray<uint32_t> HBFont::features() const +{ + std::unordered_set<uint32_t> features; + auto face = hb_font_get_face(m_font); + fillFeatures(face, HB_OT_TAG_GSUB, features); + fillFeatures(face, HB_OT_TAG_GPOS, features); + + rive::SimpleArray<uint32_t> result(features.size()); + uint32_t index = 0; + for (auto tag : features) + { + result[index++] = tag; + } + return result; } rive::Font::Axis HBFont::getAxis(uint16_t index) const { - auto face = hb_font_get_face(m_Font); + auto face = hb_font_get_face(m_font); assert(index < hb_ot_var_get_axis_count(face)); unsigned n = 1; hb_ot_var_axis_info_t info; @@ -149,27 +235,18 @@ uint16_t HBFont::getAxisCount() const { - auto face = hb_font_get_face(m_Font); + auto face = hb_font_get_face(m_font); return (uint16_t)hb_ot_var_get_axis_count(face); } float HBFont::getAxisValue(uint32_t axisTag) const { - auto face = hb_font_get_face(m_Font); - uint32_t length; - - // Check if we have a sepecified value. - const float* values = hb_font_get_var_coords_design(m_Font, &length); - for (uint32_t i = 0; i < length; ++i) + auto itr = m_axisValues.find(axisTag); + if (itr != m_axisValues.end()) { - hb_ot_var_axis_info_t info; - uint32_t n = 1; - hb_ot_var_get_axis_infos(face, i, &n, &info); - if (info.tag == axisTag) - { - return values[i]; - } + return itr->second; } + auto face = hb_font_get_face(m_font); // No value specified, we're using a default. uint32_t axisCount = hb_ot_var_get_axis_count(face); @@ -186,52 +263,59 @@ return 0.0f; } -std::vector<rive::Font::Coord> HBFont::getCoords() const +uint32_t HBFont::getFeatureValue(uint32_t featureTag) const { - auto axes = this->getAxes(); - // const int count = (int)axes.size(); - - unsigned length; - const float* values = hb_font_get_var_coords_design(m_Font, &length); - - std::vector<rive::Font::Coord> coords(length); - for (unsigned i = 0; i < length; ++i) + auto itr = m_featureValues.find(featureTag); + if (itr != m_featureValues.end()) { - coords[i] = {axes[i].tag, values[i]}; + return itr->second; } - return coords; + return (uint32_t)-1; } -rive::rcp<rive::Font> HBFont::makeAtCoords(rive::Span<const Coord> coords) const +rive::rcp<rive::Font> HBFont::withOptions(rive::Span<const Coord> coords, + rive::Span<const Feature> features) const { - AutoSTArray<16, hb_variation_t> vars(coords.size()); + // Merges previous options with current ones. + std::unordered_map<hb_tag_t, float> axisValues = m_axisValues; for (size_t i = 0; i < coords.size(); ++i) { - vars[i] = {coords[i].axis, coords[i].value}; + axisValues[coords[i].axis] = coords[i].value; } - auto font = hb_font_create_sub_font(m_Font); + + AutoSTArray<16, hb_variation_t> vars(axisValues.size()); + size_t i = 0; + for (auto itr = axisValues.begin(); itr != axisValues.end(); itr++) + { + vars[i++] = {itr->first, itr->second}; + } + + auto font = hb_font_create_sub_font(m_font); hb_font_set_variations(font, vars.data(), (unsigned int)vars.size()); - return rive::rcp<rive::Font>(new HBFont(font)); + std::vector<hb_feature_t> hbFeatures; + std::unordered_map<hb_tag_t, uint32_t> featureValues = m_featureValues; + for (auto feature : features) + { + featureValues[feature.tag] = feature.value; + } + for (auto itr = featureValues.begin(); itr != featureValues.end(); itr++) + { + hbFeatures.push_back( + {itr->first, itr->second, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}); + } + + return rive::rcp<rive::Font>(new HBFont(font, axisValues, featureValues, hbFeatures)); } rive::RawPath HBFont::getPath(rive::GlyphID glyph) const { rive::RawPath rpath; - hb_font_get_glyph_shape(m_Font, glyph, m_DrawFuncs, &rpath); + hb_font_get_glyph_shape(m_font, glyph, m_drawFuncs, &rpath); return rpath; } /////////////////////////////////////////////////////////// -const hb_feature_t gFeatures[] = { - {'liga', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}, - {'dlig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}, - // {'clig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}, - // {'calt', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}, - {'kern', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END}, -}; -constexpr int gNumFeatures = sizeof(gFeatures) / sizeof(gFeatures[0]); - static rive::GlyphRun shape_run(const rive::Unichar text[], const rive::TextRun& tr, unsigned textOffset) @@ -246,7 +330,10 @@ hb_buffer_set_language(buf, hb_language_get_default()); auto hbfont = (HBFont*)tr.font.get(); - hb_shape(hbfont->m_Font, buf, gFeatures, gNumFeatures); + hb_shape(hbfont->m_font, + buf, + hbfont->m_features.data(), + (unsigned int)hbfont->m_features.size()); unsigned int glyph_count; hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count); @@ -510,7 +597,7 @@ bool HBFont::hasGlyph(rive::Span<const rive::Unichar> missing) { hb_codepoint_t glyph; - return !missing.empty() && hb_font_get_nominal_glyph(m_Font, missing[0], &glyph); + return !missing.empty() && hb_font_get_nominal_glyph(m_font, missing[0], &glyph); } #endif
diff --git a/src/text/text_engine.cpp b/src/text/text_engine.cpp deleted file mode 100644 index c57ee77..0000000 --- a/src/text/text_engine.cpp +++ /dev/null
@@ -1,21 +0,0 @@ -#ifdef WITH_RIVE_TEXT -#include "rive/text_engine.hpp" - -using namespace rive; - -std::vector<Font::Axis> Font::getAxes() const -{ - std::vector<Font::Axis> axes; - const uint16_t count = getAxisCount(); - if (count > 0) - { - axes.resize(count); - - for (uint16_t i = 0; i < count; ++i) - { - axes.push_back(getAxis(i)); - } - } - return axes; -} -#endif \ No newline at end of file
diff --git a/test/fallback_font_test.cpp b/test/font_test.cpp similarity index 62% rename from test/fallback_font_test.cpp rename to test/font_test.cpp index b42a428..0320404 100644 --- a/test/fallback_font_test.cpp +++ b/test/font_test.cpp
@@ -1,12 +1,9 @@ -/* - * Copyright 2022 Rive - */ - #include <rive/simple_array.hpp> #include <catch.hpp> #include <rive/text_engine.hpp> #include <rive/text/font_hb.hpp> #include "rive/text/utf.hpp" +#include <string> using namespace rive; @@ -27,7 +24,7 @@ static rcp<Font> loadFont(const char* filename) { - FILE* fp = fopen("../../test/assets/RobotoFlex.ttf", "rb"); + FILE* fp = fopen(filename, "rb"); REQUIRE(fp != nullptr); fseek(fp, 0, SEEK_END); @@ -82,11 +79,12 @@ auto font = loadFont("../../test/assets/RobotoFlex.ttf"); REQUIRE(font != nullptr); - std::vector<rive::Font::Axis> axes = font->getAxes(); + auto count = font->getAxisCount(); bool hasWeight = false; - for (rive::Font::Axis axis : axes) + for (uint16_t i = 0; i < count; i++) { + auto axis = font->getAxis(i); if (axis.tag == 2003265652) { REQUIRE(axis.def == 400.0f); @@ -100,11 +98,54 @@ float value = font->getAxisValue(2003265652); REQUIRE(value == 400.0f); + REQUIRE(font->getAxisValue(2003072104) == 100.0f); + rive::Font::Coord coord = {2003265652, 800.0f}; rive::rcp<rive::Font> vfont = font->makeAtCoords(rive::Span<HBFont::Coord>(&coord, 1)); REQUIRE(vfont->getAxisValue(2003265652) == 800.0f); - rive::Font::Coord coord2 = {2003265652, 822.0f}; + rive::Font::Coord coord2 = {2003072104, 122.0f}; rive::rcp<rive::Font> vfont2 = vfont->makeAtCoords(rive::Span<HBFont::Coord>(&coord2, 1)); - REQUIRE(vfont2->getAxisValue(2003265652) == 822.0f); + REQUIRE(vfont2->getAxisValue(2003072104) == 122.0f); + // Should also still have the first axis value we set. + REQUIRE(vfont2->getAxisValue(2003265652) == 800.0f); +} + +static std::string tagToString(uint32_t tag) +{ + std::string tag_name; + tag_name += ((char)((tag & 0xff000000) >> 24)); + tag_name += ((char)((tag & 0x00ff0000) >> 16)); + tag_name += ((char)((tag & 0x0000ff00) >> 8)); + tag_name += ((char)((tag & 0x000000ff))); + return tag_name; +} + +static bool hasTag(std::vector<std::string> featureStrings, std::string tag) +{ + return std::find(std::begin(featureStrings), std::end(featureStrings), tag) != + std::end(featureStrings); +} + +TEST_CASE("font features load as expected", "[text]") +{ + REQUIRE(fallbackFonts.empty()); + auto font = loadFont("../../test/assets/RobotoFlex.ttf"); + REQUIRE(font != nullptr); + + rive::SimpleArray<uint32_t> features = font->features(); + std::vector<std::string> featureStrings; + for (auto feature : features) + { + featureStrings.push_back(tagToString(feature)); + } + REQUIRE(features.size() == 7); + + REQUIRE(hasTag(featureStrings, "mkmk")); + REQUIRE(hasTag(featureStrings, "kern")); + REQUIRE(hasTag(featureStrings, "rvrn")); + REQUIRE(hasTag(featureStrings, "mark")); + REQUIRE(hasTag(featureStrings, "locl")); + REQUIRE(hasTag(featureStrings, "pnum")); + REQUIRE(hasTag(featureStrings, "liga")); }