Implement RenderFont using CoreText

Create a backend for RenderFont that uses Apple's CoreText, so we don't have to include a copy of HarfBuzz (~750K).

Had to (re)discover that Apple tries to 'help' when a font as an optical-size variation axis (opsz). In this case, Apple auto-sets the axis value to the font's pointsize... even if the caller didn't want that. Since we want to have our fonts at a huge size (to keep precision), this code has to...

1. Determine if the font has a opsz axis
2. If so, use that axis' default value for the font's pointsize, and
3.    set the font's xform to account for that (resulting in the expected large size)

Diffs=
db49cdfaa Implement RenderFont using CoreText
diff --git a/.rive_head b/.rive_head
index 2684f4a..c56723d 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-1dca3bf6aaf61e675d5b8ce062e51444693bd60b
+db49cdfaa1ec3a56ad7c5d26dd574e67498a96ae
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua
index 2726453..898b534 100644
--- a/skia/renderer/build/premake5.lua
+++ b/skia/renderer/build/premake5.lua
@@ -36,6 +36,7 @@
 
     files {
         "../src/skia_factory.cpp",
+        "../src/cg_skia_factory.cpp",
     }
 
     buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-Werror=format"}
diff --git a/skia/viewer/src/cg_skia_factory.hpp b/skia/renderer/include/cg_skia_factory.hpp
similarity index 100%
rename from skia/viewer/src/cg_skia_factory.hpp
rename to skia/renderer/include/cg_skia_factory.hpp
diff --git a/skia/renderer/include/line_breaker.hpp b/skia/renderer/include/line_breaker.hpp
index aa45448..eff7d2d 100644
--- a/skia/renderer/include/line_breaker.hpp
+++ b/skia/renderer/include/line_breaker.hpp
@@ -1,3 +1,7 @@
+/*
+ * Copyright 2022 Rive
+ */
+
 #ifndef _RIVE_RENDER_GLYPH_LINE_H_
 #define _RIVE_RENDER_GLYPH_LINE_H_
 
diff --git a/skia/renderer/include/mac_utils.hpp b/skia/renderer/include/mac_utils.hpp
new file mode 100644
index 0000000..19b1090
--- /dev/null
+++ b/skia/renderer/include/mac_utils.hpp
@@ -0,0 +1,93 @@
+#ifndef _RIVE_MAC_UTILS_HPP_
+#define _RIVE_MAC_UTILS_HPP_
+
+#include "rive/rive_types.hpp"
+#include <string>
+
+#if defined(RIVE_BUILD_FOR_OSX)
+    #include <ApplicationServices/ApplicationServices.h>
+#elif defined(SK_BUILD_FOR_IOS)
+    #include <CoreFoundation/CoreFoundation.h>
+#endif
+
+template <size_t N, typename T> class AutoSTArray {
+    T m_storage[N];
+    T* m_ptr;
+    const size_t m_count;
+
+public:
+    AutoSTArray(size_t n) : m_count(n) {
+        m_ptr = m_storage;
+        if (n > N) {
+            m_ptr = new T[n];
+        }
+    }
+    ~AutoSTArray() {
+        if (m_ptr != m_storage) {
+            delete[] m_ptr;
+        }
+    }
+
+    T* data() const { return m_ptr; }
+
+    T& operator[](size_t index) {
+        assert(index < m_count);
+        return m_ptr[index];
+    }
+};
+
+constexpr inline uint32_t make_tag(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
+    return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+static inline std::string tag2str(uint32_t tag) {
+    std::string str = "abcd";
+    str[0] = (tag >> 24) & 0xFF;
+    str[1] = (tag >> 16) & 0xFF;
+    str[2] = (tag >>  8) & 0xFF;
+    str[3] = (tag >>  0) & 0xFF;
+    return str;
+}
+
+template <typename T> class AutoCF {
+    T m_Obj;
+public:
+    AutoCF(T obj) : m_Obj(obj) {}
+    ~AutoCF() { if (m_Obj) CFRelease(m_Obj); }
+
+    operator T() const { return m_Obj; }
+    operator bool() const { return m_Obj != nullptr; }
+    T get() const { return m_Obj; }
+};
+
+static inline float find_float(CFDictionaryRef dict, const void* key) {
+    auto num = (CFNumberRef)CFDictionaryGetValue(dict, key);
+    assert(num);
+    assert(CFNumberIsFloatType(num));
+    float value = 0;
+    CFNumberGetValue(num, kCFNumberFloat32Type, &value);
+    return value;
+}
+
+static inline uint32_t find_u32(CFDictionaryRef dict, const void* key) {
+    auto num = (CFNumberRef)CFDictionaryGetValue(dict, key);
+    assert(num);
+    assert(!CFNumberIsFloatType(num));
+    uint32_t value = 0;
+    CFNumberGetValue(num, kCFNumberSInt32Type, &value);
+    return value;
+}
+
+static inline uint32_t number_as_u32(CFNumberRef num) {
+    uint32_t value;
+    CFNumberGetValue(num, kCFNumberSInt32Type, &value);
+    return value;
+}
+
+static inline float number_as_float(CFNumberRef num) {
+    float value;
+    CFNumberGetValue(num, kCFNumberFloat32Type, &value);
+    return value;
+}
+
+#endif
diff --git a/skia/renderer/include/renderer_utils.hpp b/skia/renderer/include/renderer_utils.hpp
new file mode 100644
index 0000000..80cb528
--- /dev/null
+++ b/skia/renderer/include/renderer_utils.hpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERER_UTILS_HPP_
+#define _RIVE_RENDERER_UTILS_HPP_
+
+#include "rive/rive_types.hpp"
+#include "rive/core/type_conversions.hpp"
+#include <string>
+
+template <size_t N, typename T> class AutoSTArray {
+    T m_storage[N];
+    T* m_ptr;
+    const size_t m_count;
+
+public:
+    AutoSTArray(size_t n) : m_count(n) {
+        m_ptr = m_storage;
+        if (n > N) {
+            m_ptr = new T[n];
+        }
+    }
+    ~AutoSTArray() {
+        if (m_ptr != m_storage) {
+            delete[] m_ptr;
+        }
+    }
+
+    size_t size() const { return m_count; }
+    int count() const { return rive::castTo<int>(m_count); }
+
+    T* data() const { return m_ptr; }
+
+    T& operator[](size_t index) {
+        assert(index < m_count);
+        return m_ptr[index];
+    }
+};
+
+constexpr inline uint32_t make_tag(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
+    return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+static inline std::string tag2str(uint32_t tag) {
+    std::string str = "abcd";
+    str[0] = (tag >> 24) & 0xFF;
+    str[1] = (tag >> 16) & 0xFF;
+    str[2] = (tag >>  8) & 0xFF;
+    str[3] = (tag >>  0) & 0xFF;
+    return str;
+}
+
+#endif
diff --git a/skia/renderer/include/renderfont_coretext.hpp b/skia/renderer/include/renderfont_coretext.hpp
new file mode 100644
index 0000000..c5dc223
--- /dev/null
+++ b/skia/renderer/include/renderfont_coretext.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERFONT_CORETEXT_HPP_
+#define _RIVE_RENDERFONT_CORETEXT_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/render_text.hpp"
+
+#if defined(RIVE_BUILD_FOR_OSX)
+    #include <ApplicationServices/ApplicationServices.h>
+#elif defined(SK_BUILD_FOR_IOS)
+    #include <CoreText/CoreText.h>
+#endif
+
+class CoreTextRenderFont : public rive::RenderFont {
+public:
+    CTFontRef m_font;
+    const std::vector<Axis> m_axes;
+    const std::vector<Coord> m_coords;
+
+    // We assume ownership of font!
+    CoreTextRenderFont(CTFontRef, std::vector<Axis>);
+    ~CoreTextRenderFont() override;
+
+    std::vector<Axis> getAxes() const override { return m_axes; }
+    std::vector<Coord> getCoords() const override { return m_coords; }
+    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/viewer/src/cg_skia_factory.cpp b/skia/renderer/src/cg_skia_factory.cpp
similarity index 86%
rename from skia/viewer/src/cg_skia_factory.cpp
rename to skia/renderer/src/cg_skia_factory.cpp
index 7ffad4d..787d5a8 100644
--- a/skia/viewer/src/cg_skia_factory.cpp
+++ b/skia/renderer/src/cg_skia_factory.cpp
@@ -2,12 +2,14 @@
  * Copyright 2022 Rive
  */
 
-#include "cg_skia_factory.hpp"
 #include "rive/core/type_conversions.hpp"
 #include <vector>
 
 #ifdef RIVE_BUILD_FOR_APPLE
 
+#include "cg_skia_factory.hpp"
+#include "mac_utils.hpp"
+
 #if defined(RIVE_BUILD_FOR_OSX)
     #include <ApplicationServices/ApplicationServices.h>
 #elif defined(RIVE_BUILD_FOR_IOS)
@@ -15,18 +17,6 @@
     #include <ImageIO/ImageIO.h>
 #endif
 
-// Helper that remembers to call CFRelease when an object goes out of scope.
-template <typename T> class AutoCF {
-    T m_Obj;
-public:
-    AutoCF(T obj) : m_Obj(obj) {}
-    ~AutoCF() { if (m_Obj) CFRelease(m_Obj); }
-
-    operator T() const { return m_Obj; }
-    operator bool() const { return m_Obj != nullptr; }
-    T get() const { return m_Obj; }
-};
-
 using namespace rive;
 
 std::vector<uint8_t> CGSkiaFactory::platformDecode(Span<const uint8_t> span,
diff --git a/skia/renderer/src/line_breaker.cpp b/skia/renderer/src/line_breaker.cpp
index 53f8db5..8031013 100644
--- a/skia/renderer/src/line_breaker.cpp
+++ b/skia/renderer/src/line_breaker.cpp
@@ -1,3 +1,7 @@
+/*
+ * Copyright 2022 Rive
+ */
+
 #include "line_breaker.hpp"
 
 using namespace rive;
diff --git a/skia/renderer/src/renderfont_coretext.cpp b/skia/renderer/src/renderfont_coretext.cpp
new file mode 100644
index 0000000..9258f5c
--- /dev/null
+++ b/skia/renderer/src/renderfont_coretext.cpp
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "renderfont_coretext.hpp"
+#include "mac_utils.hpp"
+
+#include "rive/factory.hpp"
+#include "rive/render_text.hpp"
+#include "rive/core/type_conversions.hpp"
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+#if defined(RIVE_BUILD_FOR_OSX)
+    #include <ApplicationServices/ApplicationServices.h>
+#elif defined(SK_BUILD_FOR_IOS)
+    #include <CoreText/CoreText.h>
+    #include <CoreText/CTFontManager.h>
+    #include <CoreGraphics/CoreGraphics.h>
+    #include <CoreFoundation/CoreFoundation.h>
+#endif
+
+constexpr int kStdScale = 2048;
+constexpr float gInvScale = 1.0f / kStdScale;
+
+static std::vector<rive::RenderFont::Axis> compute_axes(CTFontRef font) {
+    std::vector<rive::RenderFont::Axis> axes;
+
+    AutoCF array = CTFontCopyVariationAxes(font);
+    if (auto count = array.get() ? CFArrayGetCount(array.get()) : 0) {
+        axes.reserve(count);
+
+        for (auto i = 0; i < count; ++i) {
+            auto axis = (CFDictionaryRef)CFArrayGetValueAtIndex(array, i);
+
+            auto tag = find_u32(axis, kCTFontVariationAxisIdentifierKey);
+            auto min = find_float(axis, kCTFontVariationAxisMinimumValueKey);
+            auto def = find_float(axis, kCTFontVariationAxisDefaultValueKey);
+            auto max = find_float(axis, kCTFontVariationAxisMaximumValueKey);
+       //     printf("%08X %g %g %g\n", tag, min, def, max);
+
+            axes.push_back({tag, min, def, max});
+        }
+    }
+    return axes;
+}
+
+static std::vector<rive::RenderFont::Coord> compute_coords(CTFontRef font) {
+    std::vector<rive::RenderFont::Coord> coords(0);
+    AutoCF dict = CTFontCopyVariation(font);
+    if (dict) {
+        int count = CFDictionaryGetCount(dict);
+        if (count > 0) {
+            coords.resize(count);
+
+            AutoSTArray<100, const void*> ptrs(count * 2);
+            const void** keys = &ptrs[0];
+            const void** values = &ptrs[count];
+            CFDictionaryGetKeysAndValues(dict, keys, values);
+            for (int i = 0; i < count; ++i) {
+                uint32_t tag = number_as_u32((CFNumberRef)keys[i]);
+                float value = number_as_float((CFNumberRef)values[i]);
+//                printf("[%d] %08X %s %g\n", i, tag, tag2str(tag).c_str(), value);
+                coords[i] = {tag, value};
+            }
+        }
+    }
+    return coords;
+}
+
+rive::rcp<rive::RenderFont> CoreTextRenderFont::Decode(rive::Span<const uint8_t> span) {
+    AutoCF data = CFDataCreate(nullptr, span.data(), span.size());  // makes a copy
+    if (!data) {
+        assert(false);
+        return nullptr;
+    }
+
+    AutoCF desc = CTFontManagerCreateFontDescriptorFromData(data.get());
+    if (!desc) {
+        assert(false);
+        return nullptr;
+    }
+
+    CTFontOptions options = kCTFontOptionsPreventAutoActivation;
+
+    // Note: this may set the 'opsz' axis, which we need to undo...
+    auto ctfont = CTFontCreateWithFontDescriptorAndOptions(desc.get(), kStdScale, nullptr, options);
+    if (!ctfont) {
+        assert(false);
+        return nullptr;
+    }
+
+    auto axes = compute_axes(ctfont);
+    if (axes.size() > 0) {
+        constexpr uint32_t kOPSZ = make_tag('o', 'p', 's', 'z');
+        for (const auto& ax : axes) {
+            if (ax.tag == kOPSZ) {
+                auto xform = CGAffineTransformMakeScale(kStdScale / ax.def, kStdScale / ax.def);
+                // Recreate the font at this size, but with a balancing transform,
+                // so we get the 'default' shapes w.r.t. the opsz axis
+                auto newfont = CTFontCreateCopyWithAttributes(ctfont, ax.def, &xform, nullptr);
+                CFRelease(ctfont);
+                ctfont = newfont;
+                break;
+            }
+        }
+    }
+
+    return rive::rcp<rive::RenderFont>(new CoreTextRenderFont(ctfont, std::move(axes)));
+}
+
+static rive::RenderFont::LineMetrics make_lmx(CTFontRef font) {
+    return {
+        (float) -CTFontGetAscent(font) * gInvScale,
+        (float) CTFontGetDescent(font) * gInvScale,
+    };
+}
+
+CoreTextRenderFont::CoreTextRenderFont(CTFontRef font,
+                                       std::vector<rive::RenderFont::Axis> axes) :
+    rive::RenderFont(make_lmx(font)),
+    m_font(font),                       // we take ownership of font
+    m_axes(std::move(axes)),
+    m_coords(compute_coords(font))
+{}
+
+CoreTextRenderFont::~CoreTextRenderFont() {
+    CFRelease(m_font);
+}
+
+rive::rcp<rive::RenderFont> CoreTextRenderFont::makeAtCoords(rive::Span<const Coord> coords) const {
+    AutoCF vars = CFDictionaryCreateMutable(kCFAllocatorDefault, coords.size(),
+                                             &kCFTypeDictionaryKeyCallBacks,
+                                            &kCFTypeDictionaryValueCallBacks);
+    for (const auto& c : coords) {
+        AutoCF tagNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &c.axis);
+        AutoCF valueNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat32Type, &c.value);
+        CFDictionaryAddValue(vars.get(), tagNum.get(), valueNum.get());
+    }
+
+    AutoCF attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1,
+                                             &kCFTypeDictionaryKeyCallBacks,
+                                             &kCFTypeDictionaryValueCallBacks);
+    CFDictionarySetValue(attrs.get(), kCTFontVariationAttribute, vars.get());
+    
+    AutoCF desc = (CTFontDescriptorRef)CTFontDescriptorCreateWithAttributes(attrs.get());
+
+    auto font = CTFontCreateCopyWithAttributes(m_font, 0, nullptr, desc.get());
+
+    return rive::rcp<rive::RenderFont>(new CoreTextRenderFont(font, compute_axes(font)));
+}
+
+static void apply_element(void *ctx, const CGPathElement *element) {
+    auto path = (rive::RawPath*)ctx;
+    const CGPoint* points = element->points;
+
+    switch (element->type) {
+        case kCGPathElementMoveToPoint:
+            path->moveTo(points[0].x, points[0].y);
+            break;
+
+        case kCGPathElementAddLineToPoint:
+            path->lineTo(points[0].x, points[0].y);
+            break;
+
+        case kCGPathElementAddQuadCurveToPoint:
+            path->quadTo(points[0].x, points[0].y,
+                         points[1].x, points[1].y);
+            break;
+
+        case kCGPathElementAddCurveToPoint:
+            path->cubicTo(points[0].x, points[0].y,
+                          points[1].x, points[1].y,
+                          points[2].x, points[2].y);
+            break;
+
+        case kCGPathElementCloseSubpath:
+            path->close();
+            break;
+
+        default:
+            assert(false);
+            break;
+    }
+}
+
+rive::RawPath CoreTextRenderFont::getPath(rive::GlyphID glyph) const {
+    rive::RawPath rpath;
+
+    AutoCF cgPath = CTFontCreatePathForGlyph(m_font, glyph, nullptr);
+    if (!cgPath) {
+        return rpath;
+    }
+
+    CGPathApply(cgPath.get(), &rpath, apply_element);
+    rpath.transformInPlace(rive::Mat2D::fromScale(gInvScale, -gInvScale));
+    return rpath;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+
+struct AutoUTF16 {
+    AutoSTArray<1024, uint16_t> array;
+
+    AutoUTF16(const rive::Unichar uni[], int count) : array(count) {
+        for (int i = 0; i < count; ++i) {
+            array[i] = rive::castTo<uint16_t>(uni[i]);
+        }
+    }
+};
+
+static float add_run(rive::RenderGlyphRun* gr, CTRunRef run,
+                     uint32_t textStart, float textSize, float startX) {
+    if (auto count = CTRunGetGlyphCount(run)) {
+        const float scale = textSize * gInvScale;
+
+        gr->glyphs.resize(count);
+        gr->xpos.resize(count + 1);
+        gr->textOffsets.resize(count);
+
+        const auto txStart = textStart + CTRunGetStringRange(run).location;
+
+        CTRunGetGlyphs(run, {0, count}, gr->glyphs.data());
+
+        AutoSTArray<1024, CFIndex> indices(count);
+        AutoSTArray<1024, CGSize> advances(count);
+
+        CTRunGetAdvances(run, {0, count}, advances.data());
+        CTRunGetStringIndices(run, {0, count}, indices.data());
+
+        for (CFIndex i = 0; i < count; ++i) {
+            gr->xpos[i] = startX;
+            gr->textOffsets[i] = txStart + indices[i];
+            startX += advances[i].width * scale;
+        }
+        gr->xpos[count] = startX;
+    }
+    return startX;
+}
+
+std::vector<rive::RenderGlyphRun>
+CoreTextRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
+                                rive::Span<const rive::RenderTextRun> truns) const {
+    std::vector<rive::RenderGlyphRun> gruns;
+    gruns.reserve(truns.size());
+
+    uint32_t unicharIndex = 0;
+    float startX = 0;
+    for (const auto& tr : truns) {
+        CTFontRef font = ((CoreTextRenderFont*)tr.font.get())->m_font;
+
+        AutoUTF16 utf16(&text[unicharIndex], tr.unicharCount);
+        AutoCF string = CFStringCreateWithCharactersNoCopy(nullptr, utf16.array.data(),
+                                                           tr.unicharCount, kCFAllocatorNull);
+
+        AutoCF attr = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
+                                               &kCFTypeDictionaryKeyCallBacks,
+                                               &kCFTypeDictionaryValueCallBacks);
+        CFDictionaryAddValue(attr.get(), kCTFontAttributeName, font);
+
+        AutoCF attrString = CFAttributedStringCreate(kCFAllocatorDefault, string.get(), attr.get());
+
+        AutoCF typesetter = CTTypesetterCreateWithAttributedString(attrString.get());
+
+        AutoCF line = CTTypesetterCreateLine(typesetter.get(), {0, tr.unicharCount});
+
+        CFArrayRef run_array = CTLineGetGlyphRuns(line.get());
+        CFIndex runCount = CFArrayGetCount(run_array);
+        for (CFIndex i = 0; i < runCount; ++i) {
+            rive::RenderGlyphRun grun;
+            startX = add_run(&grun, (CTRunRef)CFArrayGetValueAtIndex(run_array, i),
+                             unicharIndex, tr.size, startX);
+            if (grun.glyphs.size() > 0) {
+                grun.font = tr.font;
+                grun.size = tr.size;
+                gruns.push_back(std::move(grun));
+            }
+        }
+        unicharIndex += tr.unicharCount;
+    }
+
+    return gruns;
+}
+#endif
diff --git a/skia/renderer/src/renderfont_hb.cpp b/skia/renderer/src/renderfont_hb.cpp
index 7d9c441..8e5570f 100644
--- a/skia/renderer/src/renderfont_hb.cpp
+++ b/skia/renderer/src/renderfont_hb.cpp
@@ -6,11 +6,10 @@
 
 #include "rive/factory.hpp"
 #include "rive/render_text.hpp"
+#include "renderer_utils.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(),
@@ -119,15 +118,12 @@
 }
 
 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;
+    AutoSTArray<16, hb_variation_t> vars(coords.size());
+    for (size_t i = 0; i < coords.size(); ++i) {
+        vars[i] = {coords[i].axis, coords[i].value};
     }
-
     auto font = hb_font_create_sub_font(m_Font);
-    hb_font_set_variations(font, vars.data(), count);
+    hb_font_set_variations(font, vars.data(), vars.size());
     return rive::rcp<rive::RenderFont>(new HBRenderFont(font));
 }
 
diff --git a/skia/viewer/build/premake5.lua b/skia/viewer/build/premake5.lua
index 8202faf..c0f8ee0 100644
--- a/skia/viewer/build/premake5.lua
+++ b/skia/viewer/build/premake5.lua
@@ -58,6 +58,7 @@
         "../src/**.cpp",
 
         "../../renderer/src/line_breaker.cpp",
+        "../../renderer/src/renderfont_coretext.cpp",
         "../../renderer/src/renderfont_hb.cpp",
         "../../renderer/src/renderfont_skia.cpp",
 
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp
index 386a359..77c80da 100644
--- a/skia/viewer/src/main.cpp
+++ b/skia/viewer/src/main.cpp
@@ -193,6 +193,7 @@
         canvas->drawPaint(paint);
 
         if (gContent) {
+            SkAutoCanvasRestore acr(canvas, true);
             gContent->handleDraw(canvas, elapsed);
         }
 
diff --git a/skia/viewer/src/text_content.cpp b/skia/viewer/src/text_content.cpp
index 8536cbb..cbae245 100644
--- a/skia/viewer/src/text_content.cpp
+++ b/skia/viewer/src/text_content.cpp
@@ -11,6 +11,10 @@
 #include "skia_renderer.hpp"
 #include "line_breaker.hpp"
 
+using RenderFontTextRuns = std::vector<rive::RenderTextRun>;
+using RenderFontGlyphRuns = std::vector<rive::RenderGlyphRun>;
+using RenderFontFactory = rive::rcp<rive::RenderFont> (*)(const rive::Span<const uint8_t>);
+
 static bool ws(rive::Unichar c) {
     return c <= ' ';
 }
@@ -75,7 +79,7 @@
 
 #include "renderfont_skia.hpp"
 #include "renderfont_hb.hpp"
-#include "include/core/SkData.h"
+#include "renderfont_coretext.hpp"
 
 static void draw_line(rive::Factory* factory, rive::Renderer* renderer, float x) {
     auto paint = factory->makeRenderPaint();
@@ -104,16 +108,17 @@
 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> {
+    std::vector<RenderFontGlyphRuns> m_gruns;
+
+    RenderFontTextRuns make_truns(RenderFontFactory fact) {
+        auto loader = [fact](const char filename[]) -> rive::rcp<rive::RenderFont> {
             auto bytes = ViewerContent::LoadFile(filename);
             if (bytes.size() == 0) {
+                assert(false);
                 return nullptr;
             }
-            return HBRenderFont::Decode(rive::toSpan(bytes));
+            return fact(rive::toSpan(bytes));
         };
 
         const char* fontFiles[] = {
@@ -127,49 +132,65 @@
         assert(font1);
 
         rive::RenderFont::Coord c1 = {'wght', 100.f},
-                                c2 = {'wght', 700.f};
+                                c2 = {'wght', 800.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."));
+        RenderFontTextRuns truns;
+
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "neasy"));
+        truns.push_back(append(&m_unichars, font1, 30, " fits the crown"));
+        truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
+        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));
+        return truns;
     }
 
 public:
     TextContent() {
-        this->make_truns();
+        RenderFontFactory factory[] = {
+            HBRenderFont::Decode,
+            CoreTextRenderFont::Decode,
+        };
+        for (auto f : factory) {
+            auto truns = this->make_truns(f);
+            m_gruns.push_back(truns[0].font->shapeText(rive::toSpan(m_unichars),
+                                                       rive::toSpan(truns)));
+        }
+    }
+
+    void draw(rive::Renderer* renderer, float width, const RenderFontGlyphRuns& gruns) {
+        renderer->save();
+        renderer->translate(10, 0);
+
+        renderer->save();
+        renderer->scale(3, 3);
+
+        auto lines = rive::RenderGlyphLine::BreakLines(rive::toSpan(gruns), rive::toSpan(m_breaks), width);
+
+        drawpara(&skiaFactory, renderer, rive::toSpan(lines), rive::toSpan(gruns), {0, 0});
+        draw_line(&skiaFactory, renderer, width);
+
+        renderer->restore();
+
+        renderer->restore();
     }
 
     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);
+        if (false) {
+            width += dw; if (width > 600) { dw = -dw; } if (width < 50) { dw = -dw; }
+        }
 
-        drawpara(&skiaFactory, &renderer, rive::toSpan(lines), rive::toSpan(m_gruns), {0, 0});
+        for (auto& grun : m_gruns) {
+            this->draw(&renderer, width, grun);
+            renderer.translate(1200, 0);
+        }
 
-        draw_line(&skiaFactory, &renderer, width);
-
-        renderer.restore();
-
-        renderer.restore();
     }
 
     void handleResize(int width, int height) override {}