Fallback for harfbuzz Plumbing is mostly in place Testing is just a start : have hard-coded a specific unicode font (will improve in later PRs) Design Harfbuzz backend has a function-pointer it will call to try to get addtional fonts to fill in the gaps (missing glyphs). It passes a span of some of the missing characters as a clue (though in this impl, we always just return the same fallback font). Likely to do a better job in CJK, we may want to know some language information from the client -- which we can pass along to the fallback-proc. Diffs= b4eb6b315 Fallback for harfbuzz
diff --git a/.rive_head b/.rive_head index 03577d8..13d8aaf 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -09df6f0b2bf8601a533ece9402f739c43ed92d50 +b4eb6b315ba31c8608b98799ac7f5b480a1045d4
diff --git a/skia/renderer/include/renderfont_hb.hpp b/skia/renderer/include/renderfont_hb.hpp index 084efec..dd8ae3e 100644 --- a/skia/renderer/include/renderfont_hb.hpp +++ b/skia/renderer/include/renderfont_hb.hpp
@@ -30,6 +30,15 @@ rive::Span<const rive::RenderTextRun>) const override; static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>); + + // 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 + // return a font that can draw (at least some of) them. If no font is available + // just return nullptr. + + using FallbackProc = rive::rcp<rive::RenderFont> (*)(rive::Span<const rive::Unichar>); + + static FallbackProc gFallbackProc; }; #endif
diff --git a/skia/renderer/src/renderfont_hb.cpp b/skia/renderer/src/renderfont_hb.cpp index e041a99..3b2cd1c 100644 --- a/skia/renderer/src/renderfont_hb.cpp +++ b/skia/renderer/src/renderfont_hb.cpp
@@ -11,6 +11,9 @@ #include "hb.h" #include "hb-ot.h" +// Initialized to null. Client can set this to a callback. +HBRenderFont::FallbackProc HBRenderFont::gFallbackProc; + rive::rcp<rive::RenderFont> HBRenderFont::Decode(rive::Span<const uint8_t> span) { auto blob = hb_blob_create_or_fail((const char*)span.data(), (unsigned)span.size(), @@ -165,11 +168,8 @@ }; constexpr int gNumFeatures = sizeof(gFeatures) / sizeof(gFeatures[0]); -static float shape_run(std::vector<rive::RenderGlyphRun>* gruns, - const rive::Unichar text[], - const rive::RenderTextRun& tr, - unsigned textOffset, - float xpos) { +static rive::RenderGlyphRun +shape_run(const rive::Unichar text[], const rive::RenderTextRun& tr, unsigned textOffset) { hb_buffer_t* buf = hb_buffer_create(); hb_buffer_add_utf32(buf, text, tr.unicharCount, 0, tr.unicharCount); @@ -194,22 +194,59 @@ gr.xpos.resize(glyph_count + 1); const float scale = tr.size / kStdScale; - for (unsigned int i = 0; i < glyph_count; i++) { // hb_position_t x_offset = glyph_pos[i].x_offset; // hb_position_t y_offset = glyph_pos[i].y_offset; gr.glyphs[i] = (uint16_t)glyph_info[i].codepoint; gr.textOffsets[i] = textOffset + glyph_info[i].cluster; - gr.xpos[i] = xpos; - - xpos += glyph_pos[i].x_advance * scale; + gr.xpos[i] = glyph_pos[i].x_advance * scale; } - gr.xpos[glyph_count] = xpos; - gruns->push_back(std::move(gr)); - + gr.xpos[glyph_count] = 0; // so the next run can line up snug hb_buffer_destroy(buf); - return xpos; + return gr; +} + +static rive::RenderGlyphRun +extract_subset(const rive::RenderGlyphRun& orig, size_t start, size_t end) { + rive::RenderGlyphRun subset; + subset.font = std::move(orig.font); + subset.size = orig.size; + subset.glyphs.insert(subset.glyphs.begin(), &orig.glyphs[start], &orig.glyphs[end]); + subset.textOffsets.insert( + subset.textOffsets.begin(), &orig.textOffsets[start], &orig.textOffsets[end]); + subset.xpos.insert(subset.xpos.begin(), &orig.xpos[start], &orig.xpos[end + 1]); + subset.xpos.back() = 0; // since we're now the end of a run + return subset; +} + +static void perform_fallback(rive::rcp<rive::RenderFont> fallbackFont, + std::vector<rive::RenderGlyphRun>* gruns, + const rive::Unichar text[], + const rive::RenderGlyphRun& orig) { + assert(orig.glyphs.size() > 0); + + const size_t count = orig.glyphs.size(); + size_t startI = 0; + while (startI < count) { + size_t endI = startI + 1; + if (orig.glyphs[startI] == 0) { + while (endI < count && orig.glyphs[endI] == 0) { + ++endI; + } + auto textStart = orig.textOffsets[startI]; + auto textCount = orig.textOffsets[endI - 1] - textStart + 1; + auto tr = rive::RenderTextRun{fallbackFont, orig.size, textCount}; + auto gr = shape_run(&text[textStart], tr, textStart); + gruns->push_back(std::move(gr)); + } else { + while (endI < count && orig.glyphs[endI] != 0) { + ++endI; + } + gruns->push_back(extract_subset(orig, startI, endI)); + } + startI = endI; + } } std::vector<rive::RenderGlyphRun> @@ -221,10 +258,36 @@ ///////////////// uint32_t unicharIndex = 0; - float xpos = 0; for (const auto& tr : truns) { - xpos = shape_run(&gruns, &text[unicharIndex], tr, unicharIndex, xpos); + auto gr = shape_run(&text[unicharIndex], tr, unicharIndex); unicharIndex += tr.unicharCount; + + auto end = gr.glyphs.end(); + auto iter = std::find(gr.glyphs.begin(), end, 0); + if (!gFallbackProc || iter == end) { + gruns.push_back(std::move(gr)); + } else { + // found at least 1 zero in glyphs, so need to perform font-fallback + size_t index = iter - gr.glyphs.begin(); + rive::Unichar missing = text[gr.textOffsets[index]]; + // todo: consider sending more chars if that helps choose a font + auto fallback = gFallbackProc({&missing, 1}); + if (fallback) { + perform_fallback(fallback, &gruns, text.data(), gr); + } else { + gruns.push_back(std::move(gr)); // oh well, just keep the missing glyphs + } + } + } + + // now turn the advances (widths) we stored in xpos[] into actual x-positions + float pos = 0; + for (auto& gr : gruns) { + for (auto& xp : gr.xpos) { + float adv = xp; + xp = pos; + pos += adv; + } } return gruns; }
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp index 3d42014..9676056 100644 --- a/viewer/src/viewer_content/text_content.cpp +++ b/viewer/src/viewer_content/text_content.cpp
@@ -21,6 +21,7 @@ #else #include "renderfont_hb.hpp" static RenderFontFactory gFontFactory = HBRenderFont::Decode; +#define RIVE_USING_HAFBUZZ_FONTS #endif static bool ws(rive::Unichar c) { return c <= ' '; } @@ -91,6 +92,38 @@ //////////////////////////////////////////////////////////////////////////////////// +#ifdef RIVE_USING_HAFBUZZ_FONTS +static rive::rcp<rive::RenderFont> load_fallback_font(rive::Span<const rive::Unichar> missing) { + static rive::rcp<rive::RenderFont> gFallbackFont; + + printf("missing chars:"); + for (auto m : missing) { + printf(" %X", m); + } + printf("\n"); + + if (!gFallbackFont) { + // TODO: make this more sharable for our test apps + FILE* fp = fopen("/Users/mike/fonts/Arial Unicode.ttf", "rb"); + if (!fp) { + return nullptr; + } + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + std::vector<uint8_t> bytes(size); + size_t bytesRead = fread(bytes.data(), 1, size, fp); + fclose(fp); + + assert(bytesRead == size); + gFallbackFont = HBRenderFont::Decode(rive::toSpan(bytes)); + } + return gFallbackFont; +} +#endif + static void draw_line(rive::Factory* factory, rive::Renderer* renderer, float x) { auto paint = factory->makeRenderPaint(); paint->style(rive::RenderPaintStyle::stroke); @@ -160,6 +193,10 @@ public: TextContent() { +#ifdef RIVE_USING_HAFBUZZ_FONTS + HBRenderFont::gFallbackProc = load_fallback_font; +#endif + auto truns = this->make_truns(gFontFactory); m_gruns.push_back(truns[0].font->shapeText(rive::toSpan(m_unichars), rive::toSpan(truns)));