RenderText work Introduce a new header : render_text.hpp This has new types, including a new virtual class RenderFont that clients need to subclass and provide. This PR includes two experimental implementations: - skia based (but trivial shaper : no shaping, no kerning, no intl support) - harfbuzz based (good shaper, but no support for color glyphs) Neither try to perform font substitution yet. This PR has **no impact** on Runtimes yet -- it is just play code that Viewer can test. Diffs= 5da5fa606 RenderText work
diff --git a/.rive_head b/.rive_head index b487f8a..fc25c85 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -a798ea7de2d9189a24fa9101813c1b42498f69df +5da5fa606df8b3e05ab15e9a1297e4c941d35c60
diff --git a/include/rive/render_text.hpp b/include/rive/render_text.hpp index 20d7206..f610b74 100644 --- a/include/rive/render_text.hpp +++ b/include/rive/render_text.hpp
@@ -14,8 +14,18 @@ using Unichar = int32_t; using GlyphID = uint16_t; +struct RenderTextRun; +struct RenderGlyphRun; + class RenderFont : public RefCnt { public: + struct LineMetrics { + float ascent, + descent; + }; + + const LineMetrics& lineMetrics() const { return m_LineMetrics; } + // This is experimental // -- may only be needed by Editor // -- so it may be removed from here later @@ -55,6 +65,18 @@ // relative to (0,0) with the typographic baseline at y = 0. // virtual RawPath getPath(GlyphID) const = 0; + + std::vector<RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text, + rive::Span<const rive::RenderTextRun> runs) const; + +protected: + RenderFont(const LineMetrics& lm) : m_LineMetrics(lm) {} + + virtual std::vector<RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar> text, + rive::Span<const rive::RenderTextRun> runs) const = 0; + +private: + const LineMetrics m_LineMetrics; }; struct RenderTextRun { @@ -66,10 +88,10 @@ struct RenderGlyphRun { rcp<RenderFont> font; float size; - uint32_t startTextIndex; - std::vector<GlyphID> glyphs; - std::vector<float> xpos; // xpos.size() == glyphs.size() + 1 + std::vector<GlyphID> glyphs; // [#glyphs] + std::vector<uint32_t> textOffsets; // [#glyphs] + std::vector<float> xpos; // [#glyphs + 1] }; } // namespace rive
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua index c1f7051..2726453 100644 --- a/skia/renderer/build/premake5.lua +++ b/skia/renderer/build/premake5.lua
@@ -10,7 +10,11 @@ toolset "clang" targetdir "%{cfg.system}/bin/%{cfg.buildcfg}" objdir "%{cfg.system}/obj/%{cfg.buildcfg}" - includedirs {"../include", "../../../include"} + includedirs { + "../../../../../third_party/externals/harfbuzz/src", + "../include", + "../../../include" + } if os.host() == "macosx" then links {"Cocoa.framework", "rive", "skia"} @@ -30,7 +34,9 @@ libdirs {"../../../build/%{cfg.system}/bin/%{cfg.buildcfg}"} - files {"../src/**.cpp"} + files { + "../src/skia_factory.cpp", + } buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-Werror=format"} @@ -84,6 +90,7 @@ libdirs {"../../dependencies/" .. SKIA_DIR.. "/out/arm64"} filter "configurations:debug" + buildoptions {"-g"} defines {"DEBUG"} symbols "On"
diff --git a/skia/renderer/include/line_breaker.hpp b/skia/renderer/include/line_breaker.hpp new file mode 100644 index 0000000..aa45448 --- /dev/null +++ b/skia/renderer/include/line_breaker.hpp
@@ -0,0 +1,44 @@ +#ifndef _RIVE_RENDER_GLYPH_LINE_H_ +#define _RIVE_RENDER_GLYPH_LINE_H_ + +#include "rive/render_text.hpp" + +namespace rive { + +struct RenderGlyphLine { + int startRun; + int startIndex; + int endRun; + int endIndex; + int wsRun; + int wsIndex; + float startX; + float top = 0, baseline = 0, bottom = 0; + + RenderGlyphLine(int startRun, + int startIndex, + int endRun, + int endIndex, + int wsRun, + int wsIndex, + float startX) : + startRun(startRun), + startIndex(startIndex), + endRun(endRun), + endIndex(endIndex), + wsRun(wsRun), + wsIndex(wsIndex), + startX(startX) + {} + + static std::vector<RenderGlyphLine> BreakLines(Span<const RenderGlyphRun> runs, + Span<const int> breaks, + float width); + // Compute vaues for top/baseline/bottom per line + static void ComputeLineSpacing(rive::Span<RenderGlyphLine>, + rive::Span<const RenderGlyphRun>); +}; + +} // namespace + +#endif
diff --git a/skia/renderer/include/renderfont_hb.hpp b/skia/renderer/include/renderfont_hb.hpp new file mode 100644 index 0000000..fce1980 --- /dev/null +++ b/skia/renderer/include/renderfont_hb.hpp
@@ -0,0 +1,34 @@ +/* + * Copyright 2022 Rive + */ + +#ifndef _RIVE_RENDERFONT_HB_HPP_ +#define _RIVE_RENDERFONT_HB_HPP_ + +#include "rive/factory.hpp" +#include "rive/render_text.hpp" + +struct hb_font_t; +struct hb_draw_funcs_t; + +class HBRenderFont : public rive::RenderFont { + hb_draw_funcs_t* m_DrawFuncs; + +public: + hb_font_t* m_Font; + + // We assume ownership of font! + HBRenderFont(hb_font_t* font); + ~HBRenderFont() override; + + std::vector<Axis> getAxes() const override; + std::vector<Coord> getCoords() const override; + rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override; + rive::RawPath getPath(rive::GlyphID) const override; + std::vector<rive::RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar>, + rive::Span<const rive::RenderTextRun>) const override; + + static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>); +}; + +#endif
diff --git a/skia/renderer/include/renderfont_skia.hpp b/skia/renderer/include/renderfont_skia.hpp new file mode 100644 index 0000000..c7593ba --- /dev/null +++ b/skia/renderer/include/renderfont_skia.hpp
@@ -0,0 +1,27 @@ +/* + * Copyright 2022 Rive + */ + +#ifndef _RIVE_RENDERFONT_SKIA_HPP_ +#define _RIVE_RENDERFONT_SKIA_HPP_ + +#include "rive/render_text.hpp" +#include "include/core/SkTypeface.h" + +class SkiaRenderFont : public rive::RenderFont { +public: + sk_sp<SkTypeface> m_Typeface; + + SkiaRenderFont(sk_sp<SkTypeface>); + + std::vector<Axis> getAxes() const override; + std::vector<Coord> getCoords() const override; + rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override; + rive::RawPath getPath(rive::GlyphID) const override; + std::vector<rive::RenderGlyphRun> onShapeText(rive::Span<const rive::Unichar>, + rive::Span<const rive::RenderTextRun>) const override; + + static rive::rcp<rive::RenderFont> Decode(rive::Span<const uint8_t>); +}; + +#endif
diff --git a/skia/renderer/src/line_breaker.cpp b/skia/renderer/src/line_breaker.cpp new file mode 100644 index 0000000..53f8db5 --- /dev/null +++ b/skia/renderer/src/line_breaker.cpp
@@ -0,0 +1,144 @@ +#include "line_breaker.hpp" + +using namespace rive; + +// Return the index for the run that contains the char at textOffset +static int _offsetToRunIndex(Span<const RenderGlyphRun> runs, size_t textOffset) { + assert(textOffset >= 0); + for (int i = 0; i < (int)runs.size() - 1; ++i) { + if (textOffset <= runs[i].textOffsets.back()) { + return i; + } + } + return (int)runs.size() - 1; +} + +static int textOffsetToGlyphIndex(const RenderGlyphRun& run, size_t textOffset) { + assert(textOffset >= run.textOffsets.front()); +// assert(textOffset <= run.textOffsets.back()); // not true for last run + + // todo: bsearch? + auto begin = run.textOffsets.begin(); + auto end = run.textOffsets.end(); + auto iter = std::find(begin, end, textOffset); + if (iter == end) { // end of run + return run.glyphs.size() - 1; + } + return iter - begin; +} + +std::vector<RenderGlyphLine> RenderGlyphLine::BreakLines(Span<const RenderGlyphRun> runs, + Span<const int> breaks, + float width) { + assert(breaks.size() >= 2); + + std::vector<RenderGlyphLine> lines; + int startRun = 0; + int startIndex = 0; + double xlimit = width; + + int prevRun = 0; + int prevIndex = 0; + + int wordStart = breaks[0]; + int wordEnd = breaks[1]; + int nextBreakIndex = 2; + int lineStartTextOffset = wordStart; + + for (;;) { + assert(wordStart <= wordEnd); // == means trailing spaces? + + int endRun = _offsetToRunIndex(runs, wordEnd); + int endIndex = textOffsetToGlyphIndex(runs[endRun], wordEnd); + float pos = runs[endRun].xpos[endIndex]; + bool bumpBreakIndex = true; + if (pos > xlimit) { + int wsRun = _offsetToRunIndex(runs, wordStart); + int wsIndex = textOffsetToGlyphIndex(runs[wsRun], wordStart); + + bumpBreakIndex = false; + // does just one word not fit? + if (lineStartTextOffset == wordStart) { + // walk backwards a letter at a time until we fit, stopping at + // 1 letter. + int wend = wordEnd; + while (pos > xlimit && wend - 1 > wordStart) { + wend -= 1; + prevRun = _offsetToRunIndex(runs, wend); + prevIndex = textOffsetToGlyphIndex(runs[prevRun], wend); + pos = runs[prevRun].xpos[prevIndex]; + } + assert(wend < wordEnd || wend == wordEnd && wordStart + 1 == wordEnd); + if (wend == wordEnd) { + bumpBreakIndex = true; + } + + // now reset our "whitespace" marker to just be prev, since + // by defintion we have no extra whitespace on this line + wsRun = prevRun; + wsIndex = prevIndex; + wordStart = wend; + } + + // bulid the line + const auto lineStartX = runs[startRun].xpos[startIndex]; + lines.push_back(RenderGlyphLine( + startRun, startIndex, prevRun, prevIndex, wsRun, wsIndex, lineStartX + )); + + // update for the next line + xlimit = runs[wsRun].xpos[wsIndex] + width; + startRun = prevRun = wsRun; + startIndex = prevIndex = wsIndex; + lineStartTextOffset = wordStart; + } else { + // we didn't go too far, so remember this word-end boundary + prevRun = endRun; + prevIndex = endIndex; + } + + if (bumpBreakIndex) { + if (nextBreakIndex < breaks.size()) { + wordStart = breaks[nextBreakIndex++]; + wordEnd = breaks[nextBreakIndex++]; + } else { + break; // bust out of the loop + } + } + } + // scoop up the last line (if present) + const int tailRun = runs.size() - 1; + const int tailIndex = runs[tailRun].glyphs.size(); + if (startRun != tailRun || startIndex != tailIndex) { + const auto startX = runs[startRun].xpos[startIndex]; + lines.push_back(RenderGlyphLine( + startRun, startIndex, tailRun, tailIndex, tailRun, tailIndex, startX + )); + } + + ComputeLineSpacing(toSpan(lines), runs); + + return lines; + } + +void RenderGlyphLine::ComputeLineSpacing(Span<RenderGlyphLine> lines, + Span<const RenderGlyphRun> runs) { + float Y = 0; // top of our frame + for (auto& line : lines) { + float asc = 0; + float des = 0; + for (int i = line.startRun; i <= line.wsRun; ++i) { + const auto& run = runs[i]; + + asc = std::min(asc, run.font->lineMetrics().ascent * run.size); + des = std::max(des, run.font->lineMetrics().descent * run.size); + } + line.top = Y; + Y -= asc; + line.baseline = Y; + Y += des; + line.bottom = Y; + } + // TODO: good place to perform left/center/right alignment + +}
diff --git a/skia/renderer/src/renderfont_hb.cpp b/skia/renderer/src/renderfont_hb.cpp new file mode 100644 index 0000000..7d9c441 --- /dev/null +++ b/skia/renderer/src/renderfont_hb.cpp
@@ -0,0 +1,203 @@ +/* + * Copyright 2022 Rive + */ + +#include "renderfont_hb.hpp" + +#include "rive/factory.hpp" +#include "rive/render_text.hpp" + +#include "hb.h" +#include "hb-ot.h" +//#include "harfbuzz/hb.h" +//#include "harfbuzz/hb-ot.h" + +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(), + HB_MEMORY_MODE_DUPLICATE, nullptr, nullptr); + if (blob) { + auto face = hb_face_create(blob, 0); + hb_blob_destroy(blob); + if (face) { + auto font = hb_font_create(face); + hb_face_destroy(face); + if (font) { + return rive::rcp<rive::RenderFont>(new HBRenderFont(font)); + } + } + } + return nullptr; +} + +////////////// + +constexpr int kStdScale = 2048; +constexpr float gInvScale = 1.0f / kStdScale; + +extern "C" { +void rpath_move_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x, float y, void*) { + ((rive::RawPath*)rpath)->moveTo(x * gInvScale, -y * gInvScale); +} +void rpath_line_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x1, float y1, void*) { + ((rive::RawPath*)rpath)->lineTo(x1 * gInvScale, -y1 * gInvScale); +} +void rpath_quad_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, float x1, float y1, float x2, float y2, void*) { + ((rive::RawPath*)rpath)->quadTo(x1 * gInvScale, -y1 * gInvScale, + x2 * gInvScale, -y2 * gInvScale); +} +void rpath_cubic_to(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, + float x1, float y1, float x2, float y2, float x3, float y3, void*) { + ((rive::RawPath*)rpath)->cubicTo(x1 * gInvScale, -y1 * gInvScale, + x2 * gInvScale, -y2 * gInvScale, + x3 * gInvScale, -y3 * gInvScale); +} +void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*) { + ((rive::RawPath*)rpath)->close(); +} +} + +static rive::RenderFont::LineMetrics make_lmx(hb_font_t* font) { + // premable on font... + hb_ot_font_set_funcs(font); + hb_font_set_scale(font, kStdScale, kStdScale); + + hb_font_extents_t extents; + hb_font_get_h_extents(font, &extents); + return {-extents.ascender * gInvScale, -extents.descender * gInvScale}; +} + +HBRenderFont::HBRenderFont(hb_font_t* font) : + RenderFont(make_lmx(font)), + m_Font(font) // we just take ownership, no need to call reference() +{ + 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); +} + +HBRenderFont::~HBRenderFont() { + hb_draw_funcs_destroy(m_DrawFuncs); + hb_font_destroy(m_Font); +} + +std::vector<rive::RenderFont::Axis> HBRenderFont::getAxes() const { + auto face = hb_font_get_face(m_Font); + std::vector<rive::RenderFont::Axis> axes; + + const int count = hb_ot_var_get_axis_count(face); + if (count > 0) { + axes.resize(count); + + hb_ot_var_axis_info_t info; + for (int i = 0; i < count; ++i) { + unsigned n = 1; + hb_ot_var_get_axis_infos(face, i, &n, &info); + assert(n == 1); + axes[i] = { info.tag, info.min_value, info.default_value, info.max_value }; + // printf("[%d] %08X %g %g %g\n", i, info.tag, info.min_value, info.default_value, info.max_value); + } + } + return axes; +} + +std::vector<rive::RenderFont::Coord> HBRenderFont::getCoords() 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::RenderFont::Coord> coords(length); + for (unsigned i = 0; i < length; ++i) { + coords[i] = { axes[i].tag, values[i] }; + } + return coords; +} + +rive::rcp<rive::RenderFont> HBRenderFont::makeAtCoords(rive::Span<const Coord> coords) const { + const int count = (int)coords.size(); + std::vector<hb_variation_t> vars(count); + for (int i = 0; i < count; ++i) { + vars[i].tag = coords[i].axis; + vars[i].value = coords[i].value; + } + + auto font = hb_font_create_sub_font(m_Font); + hb_font_set_variations(font, vars.data(), count); + return rive::rcp<rive::RenderFont>(new HBRenderFont(font)); +} + +rive::RawPath HBRenderFont::getPath(rive::GlyphID glyph) const { + rive::RawPath rpath; + hb_font_get_glyph_shape(m_Font, glyph, m_DrawFuncs, &rpath); + return rpath; +} + +/////////////////////////////////////////////////////////// + +std::vector<rive::RenderGlyphRun> +HBRenderFont::onShapeText(rive::Span<const rive::Unichar> text, + rive::Span<const rive::RenderTextRun> truns) const { + std::vector<rive::RenderGlyphRun> gruns; + gruns.reserve(truns.size()); + + ///////////////// + + const hb_feature_t features[] = { + { 'liga', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + { 'dlig', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + { 'kern', 1, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END }, + }; + constexpr int numFeatures = sizeof(features) / sizeof(features[0]); + + uint32_t unicharIndex = 0; + rive::Vec2D origin = {0, 0}; + for (const auto& tr : truns) { + hb_buffer_t *buf = hb_buffer_create(); + hb_buffer_add_utf32(buf, (const uint32_t*)&text[unicharIndex], tr.unicharCount, 0, tr.unicharCount); + + hb_buffer_set_direction(buf, HB_DIRECTION_LTR); + hb_buffer_set_script(buf, HB_SCRIPT_LATIN); + hb_buffer_set_language(buf, hb_language_from_string("en", -1)); + + auto hbfont = (HBRenderFont*)tr.font.get(); + hb_shape(hbfont->m_Font, buf, features, numFeatures); + + unsigned int glyph_count; + hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count); + hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count); + + // todo: check for missing glyphs, and perform font-substitution + + rive::RenderGlyphRun gr; + gr.font = tr.font; + gr.size = tr.size; + gr.glyphs.resize(glyph_count); + gr.textOffsets.resize(glyph_count); + 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] = unicharIndex + glyph_info[i].cluster; + gr.xpos[i] = origin.x; + + origin.x += glyph_pos[i].x_advance * scale; + } + gr.xpos[glyph_count] = origin.x; + gruns.push_back(std::move(gr)); + + unicharIndex += tr.unicharCount; + hb_buffer_destroy(buf); + } + + return gruns; +}
diff --git a/skia/viewer/src/fontmgr.cpp b/skia/renderer/src/renderfont_skia.cpp similarity index 69% rename from skia/viewer/src/fontmgr.cpp rename to skia/renderer/src/renderfont_skia.cpp index 4298c3c..825fa5d 100644 --- a/skia/viewer/src/fontmgr.cpp +++ b/skia/renderer/src/renderfont_skia.cpp
@@ -4,23 +4,38 @@ #include "rive/factory.hpp" #include "rive/render_text.hpp" -#include "skia_rive_fontmgr.hpp" +#include "renderfont_skia.hpp" -#include "include/core/SkTypeface.h" +#include "include/core/SkData.h" #include "include/core/SkFont.h" +#include "include/core/SkFontMetrics.h" #include "include/core/SkPath.h" +#include "include/core/SkTypeface.h" -class SkiaRenderFont : public rive::RenderFont { -public: - sk_sp<SkTypeface> m_Typeface; +static void setupFont(SkFont* font) { + font->setLinearMetrics(true); + font->setBaselineSnap(false); + font->setHinting(SkFontHinting::kNone); +} - SkiaRenderFont(sk_sp<SkTypeface> tf) : m_Typeface(std::move(tf)) {} +static rive::RenderFont::LineMetrics make_lmx(sk_sp<SkTypeface> tf) { + SkFont font(tf, 1.0f); + setupFont(&font); + SkFontMetrics metrics; + (void)font.getMetrics(&metrics); + return {metrics.fAscent, metrics.fDescent}; +} - std::vector<Axis> getAxes() const override; - std::vector<Coord> getCoords() const override; - rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override; - rive::RawPath getPath(rive::GlyphID) const override; -}; +SkiaRenderFont::SkiaRenderFont(sk_sp<SkTypeface> tf) : + RenderFont(make_lmx(tf)), + m_Typeface(std::move(tf)) +{} + +rive::rcp<rive::RenderFont> SkiaRenderFont::Decode(rive::Span<const uint8_t> span) { + auto tf = SkTypeface::MakeFromData(SkData::MakeWithCopy(span.data(), + span.size())); + return tf ? rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf))) : nullptr; +} std::vector<rive::RenderFont::Axis> SkiaRenderFont::getAxes() const { std::vector<rive::RenderFont::Axis> axes; @@ -70,12 +85,6 @@ static inline rive::Vec2D rv(SkPoint p) { return rive::Vec2D(p.fX, p.fY); } -static void setupFont(SkFont* font) { - font->setLinearMetrics(true); - font->setBaselineSnap(false); - font->setHinting(SkFontHinting::kNone); -} - rive::RawPath SkiaRenderFont::getPath(rive::GlyphID glyph) const { SkFont font(m_Typeface, 1.0f); setupFont(&font); @@ -103,62 +112,45 @@ /////////////////////////////////////////////////////////// -rive::rcp<rive::RenderFont> SkiaRiveFontMgr::decodeFont(rive::Span<const uint8_t> data) { - auto tf = SkTypeface::MakeDefault(); // ignoring data for now - return rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf))); -} - -rive::rcp<rive::RenderFont> SkiaRiveFontMgr::findFont(const char name[]) { - auto tf = SkTypeface::MakeFromName(name, SkFontStyle()); - if (!tf) { - tf = SkTypeface::MakeDefault(); - } - return rive::rcp<rive::RenderFont>(new SkiaRenderFont(std::move(tf))); -} - static float shapeRun(rive::RenderGlyphRun* grun, const rive::RenderTextRun& trun, const rive::Unichar text[], size_t textOffset, float origin) { + const int glyphCount = SkToInt(trun.unicharCount); // simple shaper, no ligatures + grun->font = trun.font; grun->size = trun.size; - grun->startTextIndex = textOffset; - grun->glyphs.resize(trun.unicharCount); - grun->xpos.resize(trun.unicharCount + 1); - const int count = SkToInt(trun.unicharCount); + grun->glyphs.resize(glyphCount); + grun->textOffsets.resize(glyphCount); + grun->xpos.resize(glyphCount + 1); SkiaRenderFont* rfont = static_cast<SkiaRenderFont*>(trun.font.get()); - rfont->m_Typeface->unicharsToGlyphs(text + textOffset, count, grun->glyphs.data()); + rfont->m_Typeface->unicharsToGlyphs(text + textOffset, glyphCount, grun->glyphs.data()); + + // simple shaper, assume one glyph per char + for (int i = 0; i < glyphCount; ++i) { + grun->textOffsets[i] = textOffset + i; + } SkFont font(rfont->m_Typeface, grun->size); setupFont(&font); // We get 'widths' from skia, but then turn them into xpos // this will write count values, but xpos has count+1 slots - font.getWidths(grun->glyphs.data(), count, grun->xpos.data()); + font.getWidths(grun->glyphs.data(), glyphCount, grun->xpos.data()); for (auto& xp : grun->xpos) { auto width = xp; xp = origin; origin += width; } - return grun->xpos.data()[count]; + return grun->xpos.data()[glyphCount]; } std::vector<rive::RenderGlyphRun> -SkiaRiveFontMgr::shapeText(rive::Span<const rive::Unichar> text, - rive::Span<const rive::RenderTextRun> truns) { - std::vector<rive::RenderGlyphRun> gruns; +SkiaRenderFont::onShapeText(rive::Span<const rive::Unichar> text, + rive::Span<const rive::RenderTextRun> truns) const { + std::vector<rive::RenderGlyphRun> gruns(truns.size()); - // sanity check - size_t count = 0; - for (const auto& tr : truns) { - count += tr.unicharCount; - } - if (count > text.size()) { - return gruns; // not enough text, so abort - } - - gruns.resize(truns.size()); int i = 0; size_t offset = 0; float origin = 0;
diff --git a/skia/viewer/build/premake5.lua b/skia/viewer/build/premake5.lua index c2d5b58..8202faf 100644 --- a/skia/viewer/build/premake5.lua +++ b/skia/viewer/build/premake5.lua
@@ -5,6 +5,10 @@ location("./") dofile(path.join(BASE_DIR, "premake5.lua")) +BASE_DIR = path.getabsolute("../../../../../third_party/harfbuzz/build") +location("./") +dofile(path.join(BASE_DIR, "premake5.lua")) + BASE_DIR = path.getabsolute("../../renderer/build") location("./") dofile(path.join(BASE_DIR, "premake5.lua")) @@ -27,7 +31,8 @@ "../../dependencies/skia/include/config", "../../dependencies/imgui", "../../dependencies", - "../../dependencies/gl3w/build/include" + "../../dependencies/gl3w/build/include", + "../../../../../third_party/externals/harfbuzz/src", } links { @@ -35,12 +40,14 @@ "IOKit.framework", "CoreVideo.framework", "rive", + "rive_harfbuzz", "skia", "rive_skia_renderer", "glfw3" } libdirs { + "../../../../../third_party/harfbuzz/build/%{cfg.buildcfg}/bin", "../../../build/%{cfg.system}/bin/%{cfg.buildcfg}", "../../dependencies/glfw_build/src", "../../dependencies/skia/out/static", @@ -49,6 +56,11 @@ files { "../src/**.cpp", + + "../../renderer/src/line_breaker.cpp", + "../../renderer/src/renderfont_hb.cpp", + "../../renderer/src/renderfont_skia.cpp", + "../../dependencies/gl3w/build/src/gl3w.c", "../../dependencies/imgui/backends/imgui_impl_glfw.cpp", "../../dependencies/imgui/backends/imgui_impl_opengl3.cpp",
diff --git a/skia/viewer/src/drawtext.cpp b/skia/viewer/src/drawtext.cpp deleted file mode 100644 index 6a5af71..0000000 --- a/skia/viewer/src/drawtext.cpp +++ /dev/null
@@ -1,89 +0,0 @@ -/* - * Copyright 2022 Rive - */ - -#include "skia_factory.hpp" -#include "skia_renderer.hpp" -#include "skia_rive_fontmgr.hpp" - -#include <string> - -static void drawrun(rive::Factory* factory, rive::Renderer* renderer, - const rive::RenderGlyphRun& run, rive::Vec2D origin) { - auto font = run.font.get(); - const auto scale = rive::Mat2D::fromScale(run.size, run.size); - auto paint = factory->makeRenderPaint(); - paint->color(0xFFFFFFFF); - - for (size_t i = 0; i < run.glyphs.size(); ++i) { - auto trans = rive::Mat2D::fromTranslate(origin.x + run.xpos[i], origin.y); - auto rawpath = font->getPath(run.glyphs[i]); - rawpath.transformInPlace(trans * scale); - auto path = factory->makeRenderPath(rawpath.points(), rawpath.verbsU8(), rive::FillRule::nonZero); - renderer->drawPath(path.get(), paint.get()); - } -} - -struct UniString { - std::vector<rive::Unichar> array; - - UniString(const char text[]) { - while (*text) { - array.push_back(*text++); - } - } -}; - -static std::string tagstr(uint32_t tag) { - std::string s("abcd"); - s[0] = (tag >> 24) & 0xFF; - s[1] = (tag >> 16) & 0xFF; - s[2] = (tag >> 8) & 0xFF; - s[3] = (tag >> 0) & 0xFF; - return s; -} - -void drawtext(rive::Factory* factory, rive::Renderer* renderer) { - static std::vector<rive::RenderGlyphRun> gruns; - - if (gruns.size() == 0) { - SkiaRiveFontMgr fmgr; - - auto font0 = fmgr.findFont("Times New Roman"); - auto font1 = fmgr.findFont("Skia"); - - if (false) { - auto axes = font1->getAxes(); - for (auto a : axes) { - printf("%s %g %g %g\n", tagstr(a.tag).c_str(), a.min, a.def, a.max); - } - auto coords = font1->getCoords(); - for (auto c : coords) { - printf("%s %g\n", tagstr(c.axis).c_str(), c.value); - } - } - - rive::RenderFont::Coord c0 = {'wght', 0.5f}, - c1 = {'wght', 2.0f}; - - UniString str("Uneasy lies the head that wears a crown."); - rive::RenderTextRun truns[] = { - { font0, 60, 1 }, - { font0, 30, 6 }, - { font1->makeAtCoord(c0), 30, 4 }, - { font1, 30, 4 }, - { font1->makeAtCoord(c1), 30, 5 }, - { font0, 30, 20 }, - }; - - gruns = fmgr.shapeText(rive::toSpan(str.array), rive::Span(truns, 6)); - } - - renderer->save(); - renderer->scale(3, 3); - for (const auto& g : gruns) { - drawrun(factory, renderer, g, {10, 50}); - } - renderer->restore(); -} -
diff --git a/skia/viewer/src/image_content.cpp b/skia/viewer/src/image_content.cpp index 91c7c4c..276ae40 100644 --- a/skia/viewer/src/image_content.cpp +++ b/skia/viewer/src/image_content.cpp
@@ -20,8 +20,8 @@ void handleImgui() override {} }; -std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[], - rive::Span<const uint8_t> bytes) { +std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[]) { + auto bytes = LoadFile(filename); auto data = SkData::MakeWithCopy(bytes.data(), bytes.size()); if (auto image = SkImage::MakeFromEncoded(data)) { return std::make_unique<ImageContent>(std::move(image));
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp index 39814bd..386a359 100644 --- a/skia/viewer/src/main.cpp +++ b/skia/viewer/src/main.cpp
@@ -29,6 +29,30 @@ std::unique_ptr<ViewerContent> gContent; +std::vector<uint8_t> ViewerContent::LoadFile(const char filename[]) { + std::vector<uint8_t> bytes; + + FILE* fp = fopen(filename, "rb"); + if (!fp) { + fprintf(stderr, "Can't find file: %s\n", filename); + return bytes; + } + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + bytes.resize(size); + size_t bytesRead = fread(bytes.data(), 1, size, fp); + fclose(fp); + + if (bytesRead != size) { + fprintf(stderr, "Failed to read all of %s\n", filename); + bytes.resize(0); + } + return bytes; +} + static void glfwCursorPosCallback(GLFWwindow* window, double x, double y) { if (gContent) { float xscale, yscale; @@ -56,21 +80,7 @@ // Just get the last dropped file for now... const char* filename = paths[count - 1]; - FILE* fp = fopen(filename, "rb"); - 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); - - if (bytesRead != size) { - fprintf(stderr, "failed to read all of %s\n", filename); - return; - } - - auto newContent = ViewerContent::FindHandler(filename, rive::toSpan(bytes)); + auto newContent = ViewerContent::FindHandler(filename); if (newContent) { gContent = std::move(newContent); gContent->handleResize(lastScreenWidth, lastScreenHeight);
diff --git a/skia/viewer/src/scene_content.cpp b/skia/viewer/src/scene_content.cpp index 364f98d..bb443d2 100644 --- a/skia/viewer/src/scene_content.cpp +++ b/skia/viewer/src/scene_content.cpp
@@ -282,9 +282,9 @@ rive::CGSkiaFactory skiaFactory; -std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[], - rive::Span<const uint8_t> data) { - if (auto file = rive::File::import(data, &skiaFactory)) { +std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[]) { + auto bytes = LoadFile(filename); + if (auto file = rive::File::import(rive::toSpan(bytes), &skiaFactory)) { return std::make_unique<SceneContent>(filename, std::move(file)); } return nullptr;
diff --git a/skia/viewer/src/skia_rive_fontmgr.hpp b/skia/viewer/src/skia_rive_fontmgr.hpp deleted file mode 100644 index 9777b11..0000000 --- a/skia/viewer/src/skia_rive_fontmgr.hpp +++ /dev/null
@@ -1,22 +0,0 @@ -/* - * Copyright 2022 Rive - */ - -#ifndef _RIVE_SKIA_FONTMGR_HPP_ -#define _RIVE_SKIA_FONTMGR_HPP_ - -#include "rive/refcnt.hpp" -#include "rive/span.hpp" -#include "rive/render_text.hpp" - -// This is a working model for how we might extend Factory.hpp -class SkiaRiveFontMgr { -public: - rive::rcp<rive::RenderFont> decodeFont(rive::Span<const uint8_t>); - rive::rcp<rive::RenderFont> findFont(const char name[]); - std::vector<rive::RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text, - rive::Span<const rive::RenderTextRun> runs); - -}; - -#endif
diff --git a/skia/viewer/src/text_content.cpp b/skia/viewer/src/text_content.cpp new file mode 100644 index 0000000..8536cbb --- /dev/null +++ b/skia/viewer/src/text_content.cpp
@@ -0,0 +1,181 @@ +/* + * Copyright 2022 Rive + */ + +#include "viewer_content.hpp" + +#include "rive/refcnt.hpp" +#include "rive/render_text.hpp" + +#include "skia_factory.hpp" +#include "skia_renderer.hpp" +#include "line_breaker.hpp" + +static bool ws(rive::Unichar c) { + return c <= ' '; +} + +std::vector<int> compute_word_breaks(rive::Span<rive::Unichar> chars) { + std::vector<int> breaks; + + const unsigned len = chars.size(); + for (unsigned i = 0; i < len;) { + // skip ws + while (i < len && ws(chars[i])) { + ++i; + } + breaks.push_back(i); // word start + // skip non-ws + while (i < len && !ws(chars[i])) { + ++i; + } + breaks.push_back(i); // word end + } + assert(breaks[breaks.size()-1] == len); + return breaks; +} + +static void drawrun(rive::Factory* factory, rive::Renderer* renderer, + const rive::RenderGlyphRun& run, unsigned startIndex, unsigned endIndex, rive::Vec2D origin) { + auto font = run.font.get(); + const auto scale = rive::Mat2D::fromScale(run.size, run.size); + auto paint = factory->makeRenderPaint(); + paint->color(0xFFFFFFFF); + + assert(startIndex >= 0 && endIndex <= run.glyphs.size()); + for (size_t i = startIndex; i < endIndex; ++i) { + auto trans = rive::Mat2D::fromTranslate(origin.x + run.xpos[i], origin.y); + auto rawpath = font->getPath(run.glyphs[i]); + rawpath.transformInPlace(trans * scale); + auto path = factory->makeRenderPath(rawpath.points(), rawpath.verbsU8(), rive::FillRule::nonZero); + renderer->drawPath(path.get(), paint.get()); + } +} + +static void drawpara(rive::Factory* factory, rive::Renderer* renderer, + rive::Span<const rive::RenderGlyphLine> lines, + rive::Span<const rive::RenderGlyphRun> runs, + rive::Vec2D origin) { + for (const auto& line : lines) { + const float x0 = runs[line.startRun].xpos[line.startIndex]; + int startGIndex = line.startIndex; + for (int runIndex = line.startRun; runIndex <= line.endRun; ++runIndex) { + const auto& run = runs[runIndex]; + int endGIndex = runIndex == line.endRun ? line.endIndex : run.glyphs.size(); + drawrun(factory, renderer, run, startGIndex, endGIndex, + {origin.x - x0, origin.y + line.baseline}); + startGIndex = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////// + +typedef rive::rcp<rive::RenderFont> (*RenderFontFactory)(rive::Span<const uint8_t>); + +#include "renderfont_skia.hpp" +#include "renderfont_hb.hpp" +#include "include/core/SkData.h" + +static void draw_line(rive::Factory* factory, rive::Renderer* renderer, float x) { + auto paint = factory->makeRenderPaint(); + paint->style(rive::RenderPaintStyle::stroke); + paint->thickness(1); + paint->color(0xFFFFFFFF); + auto path = factory->makeEmptyRenderPath(); + path->move({x, 0}); + path->line({x, 1000}); + renderer->drawPath(path.get(), paint.get()); +} + +static rive::SkiaFactory skiaFactory; + +static rive::RenderTextRun append(std::vector<rive::Unichar>* unichars, + rive::rcp<rive::RenderFont> font, + float size, const char text[]) { + uint32_t n = 0; + while (text[n]) { + unichars->push_back(text[n]); // todo: utf8 -> unichar + n += 1; + } + return { std::move(font), size, n }; +} + +class TextContent : public ViewerContent { + std::vector<rive::Unichar> m_unichars; + std::vector<int> m_breaks; + std::vector<rive::RenderTextRun> m_truns; + std::vector<rive::RenderGlyphRun> m_gruns; + + void make_truns() { + auto loader = [](const char filename[]) -> rive::rcp<rive::RenderFont> { + auto bytes = ViewerContent::LoadFile(filename); + if (bytes.size() == 0) { + return nullptr; + } + return HBRenderFont::Decode(rive::toSpan(bytes)); + }; + + const char* fontFiles[] = { + "../../test/assets/RobotoFlex.ttf", + "../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf", + }; + + auto font0 = loader(fontFiles[0]); + auto font1 = loader(fontFiles[1]); + assert(font0); + assert(font1); + + rive::RenderFont::Coord c1 = {'wght', 100.f}, + c2 = {'wght', 700.f}; + + m_truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U")); + m_truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "neasy")); + m_truns.push_back(append(&m_unichars, font1, 30, " fits the crown")); + m_truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often")); + m_truns.push_back(append(&m_unichars, font0, 30, " lies the head.")); + + m_breaks = compute_word_breaks(rive::toSpan(m_unichars)); + } + + void make_gruns() { + m_gruns = m_truns[0].font->shapeText(rive::toSpan(m_unichars), rive::toSpan(m_truns)); + } + +public: + TextContent() { + this->make_truns(); + } + + void handleDraw(SkCanvas* canvas, double) override { + rive::SkiaRenderer renderer(canvas); + + this->make_gruns(); + + renderer.save(); + renderer.translate(10, 0); + + renderer.save(); + renderer.scale(3, 3); + + static float width = 300; + static float dw = 1; + width += dw; if (width > 600) { dw = -dw; } if (width < 50) { dw = -dw; } + auto lines = rive::RenderGlyphLine::BreakLines(rive::toSpan(m_gruns), rive::toSpan(m_breaks), width); + + drawpara(&skiaFactory, &renderer, rive::toSpan(lines), rive::toSpan(m_gruns), {0, 0}); + + draw_line(&skiaFactory, &renderer, width); + + renderer.restore(); + + renderer.restore(); + } + + void handleResize(int width, int height) override {} + void handleImgui() override {} +}; + +std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[]) { + return std::make_unique<TextContent>(); +}
diff --git a/skia/viewer/src/viewer_content.hpp b/skia/viewer/src/viewer_content.hpp index 650b375..3fd2697 100644 --- a/skia/viewer/src/viewer_content.hpp +++ b/skia/viewer/src/viewer_content.hpp
@@ -23,15 +23,13 @@ virtual void handlePointerDown() {} virtual void handlePointerUp() {} - using Factory = std::unique_ptr<ViewerContent> (*)(const char filename[], - rive::Span<const uint8_t>); + using Factory = std::unique_ptr<ViewerContent> (*)(const char filename[]); // Searches all handlers and returns a content if it is found. - static std::unique_ptr<ViewerContent> FindHandler(const char filename[], - rive::Span<const uint8_t> data) { - Factory factories[] = { Scene, Image }; + static std::unique_ptr<ViewerContent> FindHandler(const char filename[]) { + Factory factories[] = { Scene, Image, Text }; for (auto f : factories) { - if (auto content = f(filename, data)) { + if (auto content = f(filename)) { return content; } } @@ -39,8 +37,11 @@ } // Private factories... - static std::unique_ptr<ViewerContent> Scene(const char[], rive::Span<const uint8_t>); - static std::unique_ptr<ViewerContent> Image(const char[], rive::Span<const uint8_t>); + static std::unique_ptr<ViewerContent> Scene(const char[]); + static std::unique_ptr<ViewerContent> Image(const char[]); + static std::unique_ptr<ViewerContent> Text(const char[]); + + static std::vector<uint8_t> LoadFile(const char path[]); }; #endif
diff --git a/src/renderer.cpp b/src/renderer.cpp index 87111ee..9780a2a 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp
@@ -110,3 +110,26 @@ RenderPath::RenderPath() { gCounter.update(kPath, 1); } RenderPath::~RenderPath() { gCounter.update(kPath, -1); } + +#include "rive/render_text.hpp" + +std::vector<RenderGlyphRun> RenderFont::shapeText(rive::Span<const rive::Unichar> text, + rive::Span<const rive::RenderTextRun> runs) const { +#ifdef DEBUG + size_t count = 0; + for (const auto& tr : runs) { + assert(tr.unicharCount > 0); + count += tr.unicharCount; + } + assert(count <= text.size()); +#endif + auto gruns = this->onShapeText(text, runs); +#ifdef DEBUG + for (const auto& gr : gruns) { + assert(gr.glyphs.size() > 0); + assert(gr.glyphs.size() == gr.textOffsets.size()); + assert(gr.glyphs.size() + 1 == gr.xpos.size()); + } +#endif + return gruns; +}
diff --git a/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..2c0891e --- /dev/null +++ b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf Binary files differ
diff --git a/test/assets/RobotoFlex.ttf b/test/assets/RobotoFlex.ttf new file mode 100644 index 0000000..4cf1ecb --- /dev/null +++ b/test/assets/RobotoFlex.ttf Binary files differ