diff --git a/.github/workflows/build-skia.yml b/.github/workflows/build-skia.yml
index 85e6643e91..032df2351d 100644
--- a/.github/workflows/build-skia.yml
+++ b/.github/workflows/build-skia.yml
@@ -57,6 +57,8 @@ jobs:
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libsvg.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libskottie.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libsksg.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libskparagraph.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libskunicode.a
- name: Upload artifacts - Android arm64
uses: actions/upload-artifact@v2
@@ -68,6 +70,8 @@ jobs:
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libsvg.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libskottie.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libsksg.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libskparagraph.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libskunicode.a
- name: Upload artifacts - Android x86
uses: actions/upload-artifact@v2
@@ -79,6 +83,8 @@ jobs:
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libsvg.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libskottie.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libsksg.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libskparagraph.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libskunicode.a
- name: Upload artifacts - Android x64
uses: actions/upload-artifact@v2
@@ -90,6 +96,8 @@ jobs:
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libsvg.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskottie.a
${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libsksg.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskparagraph.a
+ ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskunicode.a
- name: Upload artifacts - iOS xcframeworks
uses: actions/upload-artifact@v2
@@ -101,3 +109,5 @@ jobs:
${{ env.WORKING_DIRECTORY }}/package/libs/ios/libsvg.xcframework
${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskottie.xcframework
${{ env.WORKING_DIRECTORY }}/package/libs/ios/libsksg.xcframework
+ ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskparagraph.xcframework
+ ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskunicode.xcframework
diff --git a/docs/docs/text/fonts.md b/docs/docs/text/fonts.md
new file mode 100644
index 0000000000..f0153cecb1
--- /dev/null
+++ b/docs/docs/text/fonts.md
@@ -0,0 +1,155 @@
+---
+id: fonts
+title: Fonts
+sidebar_label: Fonts
+slug: /text/fonts
+---
+
+In Skia, the `FontMgr` object manages a collection of font families.
+It allows you to access fonts from the system and manage custom fonts.
+
+## Custom Fonts
+
+The `useFonts` hooks allows you to load custom fonts to be used for your Skia drawing.
+The font files should be organized by family names.
+For example:
+
+```tsx twoslash
+import {useFonts} from "@shopify/react-native-skia";
+
+const fontMgr = useFonts({
+ Roboto: [
+ require("./Roboto-Medium.ttf"),
+ require("./Roboto-Regular.ttf"),
+ require("./Roboto-Bold.ttf"),
+ ],
+ UberMove: [require("./UberMove-Medium_mono.ttf")],
+});
+if (!fontMgr) {
+ // Returns null until all fonts are loaded
+}
+// Now the fonts are available
+```
+
+Once the fonts are loaded, we provide a `matchFont` function that given a font style will return a font object that you can use directly.
+
+```tsx twoslash
+import {useFonts, Text, matchFont} from "@shopify/react-native-skia";
+
+const Demo = () => {
+ const fontMgr = useFonts({
+ Roboto: [
+ require("./Roboto-Medium.ttf"),
+ require("./Roboto-Regular.ttf"),
+ require("./Roboto-Bold.ttf"),
+ ],
+ UberMove: [require("./UberMove-Medium_mono.ttf")],
+ });
+ if (!fontMgr) {
+ return null;
+ }
+ const fontStyle = {
+ fontFamily: "Roboto",
+ fontWeight: "bold",
+ fontSize: 16
+ } as const;
+ const font = matchFont(fontStyle, fontMgr);
+ return (
+
+ );
+};
+```
+
+## System Fonts
+
+System fonts are available via `Skia.FontMgr.System()`.
+You can list system fonts via `listFontFamilies` function returns the list of available system font families.
+By default the function will list system fonts but you can pass an optional `fontMgr` object as parameter.
+
+```jsx twoslash
+import {listFontFamilies} from "@shopify/react-native-skia";
+
+console.log(listFontFamilies());
+```
+
+Output example on Android:
+```
+["sans-serif", "arial", "helvetica", "tahoma", "verdana", ...]
+```
+
+or on iOS:
+```
+["Academy Engraved LET", "Al Nile", "American Typewriter", "Apple Color Emoji", ...]
+```
+
+By default matchFont, will match fonts from the system font manager:
+
+```jsx twoslash
+import {Platform} from "react-native";
+import {Canvas, Text, matchFont, Fill, Skia} from "@shopify/react-native-skia";
+
+const fontFamily = Platform.select({ ios: "Helvetica", default: "serif" });
+const fontStyle = {
+ fontFamily,
+ fontSize: 14,
+ fontStyle: "italic",
+ fontWeight: "bold",
+};
+const font = matchFont(fontStyle);
+
+export const HelloWorld = () => {
+ return (
+
+ );
+};
+```
+
+The `fontStyle` object can have the following list of optional attributes:
+
+- `fontFamily`: The name of the font family.
+- `fontSize`: The size of the font.
+- `fontStyle`: The slant of the font. Can be `normal`, `italic`, or `oblique`.
+- `fontWeight`: The weight of the font. Can be `normal`, `bold`, or any of `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`.
+
+By default, `matchFont` uses the system font manager to match the font style. However, if you want to use your custom font manager, you can pass it as the second parameter to the `matchFont` function:
+
+```jsx
+const fontMgr = useFonts([
+ require("../../Tests/assets/Roboto-Medium.ttf"),
+ require("../../Tests/assets/Roboto-Bold.ttf"),
+]);
+
+const font = matchFont(fontStyle, fontMgr);
+```
+
+## Low-level API
+
+The basic usage of the system font manager is as follows.
+These are the APIs used behind the scene by the `matchFont` function.
+
+```tsx twoslash
+import {Platform} from "react-native";
+import {Skia, FontStyle} from "@shopify/react-native-skia";
+
+const familyName = Platform.select({ ios: "Helvetica", default: "serif" });
+const fontSize = 32;
+// Get the system font manager
+const fontMgr = Skia.FontMgr.System();
+// The custom font manager is available via Skia.TypefaceFontProvider.Make()
+const customFontMgr = Skia.TypefaceFontProvider.Make();
+// typeface needs to be loaded via Skia.Data and instanciated via
+// Skia.Typeface.MakeFreeTypeFaceFromData()
+// customFontMgr.registerTypeface(customTypeFace, "Roboto");
+
+// Matching a font
+const typeface = fontMgr.matchFamilyStyle(familyName, FontStyle.Bold);
+const font = Skia.Font(typeface, fontSize);
+```
\ No newline at end of file
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 2da855a2e0..680dbb5df5 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -67,7 +67,13 @@ const sidebars = {
collapsed: true,
type: "category",
label: "Text",
- items: ["text/text", "text/glyphs", "text/path", "text/blob"],
+ items: [
+ "text/fonts",
+ "text/text",
+ "text/glyphs",
+ "text/path",
+ "text/blob",
+ ],
},
{
collapsed: true,
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 5200b6189f..f789d2a2a0 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -664,7 +664,7 @@ SPEC CHECKSUMS:
React-jsinspector: 9885f6f94d231b95a739ef7bb50536fb87ce7539
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
react-native-safe-area-context: dfe5aa13bee37a0c7e8059d14f72ffc076d120e9
- react-native-skia: c677fe0a5d0cc7e447df20d511a7181c47e20071
+ react-native-skia: 54fc333c477cc04df78b87b03e87b7e112bc3f47
React-perflogger: 2d505bbe298e3b7bacdd9e542b15535be07220f6
React-RCTActionSheet: 0e96e4560bd733c9b37efbf68f5b1a47615892fb
React-RCTAnimation: fd138e26f120371c87e406745a27535e2c8a04ef
diff --git a/example/src/Examples/API/FontMgr.tsx b/example/src/Examples/API/FontMgr.tsx
new file mode 100644
index 0000000000..1460721db9
--- /dev/null
+++ b/example/src/Examples/API/FontMgr.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { ScrollView, useWindowDimensions } from "react-native";
+import {
+ Canvas,
+ Skia,
+ Text,
+ matchFont,
+ useFonts,
+} from "@shopify/react-native-skia";
+
+const PADDING = 16;
+
+const titleFontSize = 32;
+
+const titleText = "Fonts from the System";
+const titleY = titleFontSize + PADDING;
+const subtitleY = titleY + 14 + PADDING;
+
+const fontMgr = Skia.FontMgr.System();
+const familyNames = new Array(fontMgr.countFamilies())
+ .fill(0)
+ .map((_, i) => fontMgr.getFamilyName(i));
+
+const title2Y = subtitleY + 16 * familyNames.length + PADDING + titleFontSize;
+
+export const FontMgr = () => {
+ const { width } = useWindowDimensions();
+ const customFontMgr = useFonts({
+ Roboto: [
+ require("../../Tests/assets/Roboto-Medium.ttf"),
+ require("../../Tests/assets/Roboto-Regular.ttf"),
+ ],
+ UberMove: [require("../../Tests/assets/UberMove-Medium_mono.ttf")],
+ });
+ if (customFontMgr === null) {
+ return null;
+ }
+ const customfamilyNames = new Array(customFontMgr.countFamilies())
+ .fill(0)
+ .map((_, i) => customFontMgr.getFamilyName(i));
+ const titleFont = matchFont({
+ fontFamily: "Helvetica",
+ fontSize: titleFontSize,
+ fontWeight: "bold",
+ });
+ const subtitleFont = matchFont();
+ return (
+
+
+
+ );
+};
diff --git a/example/src/Examples/API/List.tsx b/example/src/Examples/API/List.tsx
index 71cff6072d..e05d30074e 100644
--- a/example/src/Examples/API/List.tsx
+++ b/example/src/Examples/API/List.tsx
@@ -62,6 +62,10 @@ export const examples = [
screen: "BlendModes",
title: "🎨 Blend Modes",
},
+ {
+ screen: "FontMgr",
+ title: "💬 Font Manager",
+ },
{
screen: "Data",
title: "📊 Data",
diff --git a/example/src/Examples/API/Routes.ts b/example/src/Examples/API/Routes.ts
index 0f5a596b3a..d3b7cc5764 100644
--- a/example/src/Examples/API/Routes.ts
+++ b/example/src/Examples/API/Routes.ts
@@ -12,6 +12,7 @@ export type Routes = {
SVG: undefined;
Touch: undefined;
BlendModes: undefined;
+ FontMgr: undefined;
Data: undefined;
Picture: undefined;
Checker: undefined;
diff --git a/example/src/Examples/API/index.tsx b/example/src/Examples/API/index.tsx
index b011495702..9fa0841136 100644
--- a/example/src/Examples/API/index.tsx
+++ b/example/src/Examples/API/index.tsx
@@ -23,6 +23,7 @@ import { PictureViewExample } from "./PictureView";
import { OnLayoutDemo } from "./OnLayout";
import { Snapshot } from "./Snapshot";
import { IconsExample } from "./Icons";
+import { FontMgr } from "./FontMgr";
const Stack = createNativeStackNavigator();
export const API = () => {
@@ -134,6 +135,13 @@ export const API = () => {
title: "🎨 Blend Modes",
}}
/>
+
#include
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "include/ports/SkFontMgr_android.h"
+
+#pragma clang diagnostic pop
+
namespace RNSkia {
namespace jsi = facebook::jsi;
@@ -41,6 +48,10 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext {
return SkiaOpenGLSurfaceFactory::makeOffscreenSurface(width, height);
}
+ sk_sp createFontMgr() override {
+ return SkFontMgr_New_Android(nullptr);
+ }
+
void runOnMainThread(std::function task) override {
_jniPlatformContext->runTaskOnMainThread(task);
}
diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h
index 5e71ed87f8..612286c6bb 100644
--- a/package/cpp/api/JsiSkApi.h
+++ b/package/cpp/api/JsiSkApi.h
@@ -12,6 +12,8 @@
#include "JsiSkContourMeasureIter.h"
#include "JsiSkDataFactory.h"
#include "JsiSkFont.h"
+#include "JsiSkFontMgr.h"
+#include "JsiSkFontMgrFactory.h"
#include "JsiSkImage.h"
#include "JsiSkImageFactory.h"
#include "JsiSkImageFilter.h"
@@ -39,6 +41,7 @@
#include "JsiSkShaderFactory.h"
#include "JsiSkSurfaceFactory.h"
#include "JsiSkTextBlobFactory.h"
+#include "JsiSkTypeFaceFontProviderFactory.h"
#include "JsiSkTypeface.h"
#include "JsiSkTypefaceFactory.h"
#include "JsiSkVertices.h"
@@ -100,6 +103,11 @@ class JsiSkApi : public JsiSkHostObject {
std::make_shared(context));
installReadonlyProperty("Picture",
std::make_shared(context));
+ installReadonlyProperty("FontMgr",
+ std::make_shared(context));
+ installReadonlyProperty(
+ "TypefaceFontProvider",
+ std::make_shared(context));
}
};
} // namespace RNSkia
diff --git a/package/cpp/api/JsiSkFontMgr.h b/package/cpp/api/JsiSkFontMgr.h
new file mode 100644
index 0000000000..4ea49798ad
--- /dev/null
+++ b/package/cpp/api/JsiSkFontMgr.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "JsiSkFontStyle.h"
+#include "JsiSkHostObjects.h"
+#include "RNSkLog.h"
+#include
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkFontMgr.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+namespace jsi = facebook::jsi;
+
+class JsiSkFontMgr : public JsiSkWrappingSkPtrHostObject {
+public:
+ EXPORT_JSI_API_TYPENAME(JsiSkFontMgr, "FontMgr")
+
+ JsiSkFontMgr(std::shared_ptr context,
+ sk_sp fontMgr)
+ : JsiSkWrappingSkPtrHostObject(std::move(context), fontMgr) {}
+
+ JSI_HOST_FUNCTION(countFamilies) { return getObject()->countFamilies(); }
+
+ JSI_HOST_FUNCTION(getFamilyName) {
+ auto i = static_cast(arguments[0].asNumber());
+ SkString name;
+ getObject()->getFamilyName(i, &name);
+ return jsi::String::createFromUtf8(runtime, name.c_str());
+ }
+
+ JSI_HOST_FUNCTION(matchFamilyStyle) {
+ auto name = arguments[0].asString(runtime).utf8(runtime);
+ auto fontStyle = JsiSkFontStyle::fromValue(runtime, arguments[1]);
+ auto typeface = getObject()->matchFamilyStyle(name.c_str(), *fontStyle);
+ return jsi::Object::createFromHostObject(
+ runtime, std::make_shared(getContext(), typeface));
+ }
+
+ JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFontMgr, countFamilies),
+ JSI_EXPORT_FUNC(JsiSkFontMgr, getFamilyName),
+ JSI_EXPORT_FUNC(JsiSkFontMgr, matchFamilyStyle))
+};
+
+} // namespace RNSkia
\ No newline at end of file
diff --git a/package/cpp/api/JsiSkFontMgrFactory.h b/package/cpp/api/JsiSkFontMgrFactory.h
new file mode 100644
index 0000000000..3aa5c06639
--- /dev/null
+++ b/package/cpp/api/JsiSkFontMgrFactory.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+#include "JsiSkHostObjects.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkFontMgr.h"
+#include "include/ports/SkFontMgr_data.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+namespace jsi = facebook::jsi;
+
+class JsiSkFontMgrFactory : public JsiSkHostObject {
+public:
+ JSI_HOST_FUNCTION(System) {
+ auto context = getContext();
+ static SkOnce once;
+ static sk_sp fontMgr;
+ once([&context, &runtime] { fontMgr = context->createFontMgr(); });
+ return jsi::Object::createFromHostObject(
+ runtime, std::make_shared(std::move(context), fontMgr));
+ }
+
+ JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFontMgrFactory, System))
+
+ explicit JsiSkFontMgrFactory(std::shared_ptr context)
+ : JsiSkHostObject(std::move(context)) {}
+};
+
+} // namespace RNSkia
diff --git a/package/cpp/api/JsiSkFontStyle.h b/package/cpp/api/JsiSkFontStyle.h
new file mode 100644
index 0000000000..961ea60d79
--- /dev/null
+++ b/package/cpp/api/JsiSkFontStyle.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "JsiSkHostObjects.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkFontStyle.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+namespace jsi = facebook::jsi;
+
+class JsiSkFontStyle : public JsiSkWrappingSharedPtrHostObject {
+public:
+ JSI_API_TYPENAME("FontStyle");
+
+ JsiSkFontStyle(std::shared_ptr context,
+ const SkFontStyle &fontStyle)
+ : JsiSkWrappingSharedPtrHostObject(
+ std::move(context), std::make_shared(fontStyle)) {}
+
+ /**
+ Returns the underlying object from a host object of this type
+ */
+ static std::shared_ptr fromValue(jsi::Runtime &runtime,
+ const jsi::Value &obj) {
+ const auto &object = obj.asObject(runtime);
+ if (object.isHostObject(runtime)) {
+ return object.asHostObject(runtime)->getObject();
+ } else {
+ auto weight =
+ static_cast(object.getProperty(runtime, "weight").asNumber());
+ auto width =
+ static_cast(object.getProperty(runtime, "width").asNumber());
+ auto slant = static_cast(
+ object.getProperty(runtime, "slant").asNumber());
+ SkFontStyle style(weight, width, slant);
+ return std::make_shared(style);
+ }
+ }
+
+ /**
+ Returns the jsi object from a host object of this type
+ */
+ static jsi::Value toValue(jsi::Runtime &runtime,
+ std::shared_ptr context,
+ const SkFontStyle &fontStyle) {
+ return jsi::Object::createFromHostObject(
+ runtime,
+ std::make_shared(std::move(context), fontStyle));
+ }
+};
+} // namespace RNSkia
\ No newline at end of file
diff --git a/package/cpp/api/JsiSkTypeFaceFontProvider.h b/package/cpp/api/JsiSkTypeFaceFontProvider.h
new file mode 100644
index 0000000000..013c9828cc
--- /dev/null
+++ b/package/cpp/api/JsiSkTypeFaceFontProvider.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "JsiSkFontStyle.h"
+#include "JsiSkHostObjects.h"
+#include "JsiSkTypeface.h"
+
+#include "RNSkLog.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdocumentation"
+
+#include "SkFont.h"
+#include "skparagraph/include/TypefaceFontProvider.h"
+
+#pragma clang diagnostic pop
+
+namespace RNSkia {
+
+namespace jsi = facebook::jsi;
+namespace para = skia::textlayout;
+
+class JsiSkTypefaceFontProvider
+ : public JsiSkWrappingSkPtrHostObject {
+public:
+ EXPORT_JSI_API_TYPENAME(JsiSkTypefaceFontProvider, "FontMgr")
+ JSI_EXPORT_FUNCTIONS(
+ JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, dispose),
+ JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, registerFont),
+ JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, matchFamilyStyle),
+ JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, countFamilies),
+ JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, getFamilyName))
+
+ JSI_HOST_FUNCTION(registerFont) {
+ sk_sp typeface =
+ JsiSkTypeface::fromValue(runtime, arguments[0]);
+ SkString familyName(arguments[1].asString(runtime).utf8(runtime).c_str());
+ auto result = getObject()->registerTypeface(typeface, familyName);
+ return jsi::Value::undefined();
+ }
+
+ JSI_HOST_FUNCTION(matchFamilyStyle) {
+ auto name = arguments[0].asString(runtime).utf8(runtime);
+ auto fontStyle = JsiSkFontStyle::fromValue(runtime, arguments[1]);
+ auto typeface = getObject()->matchFamilyStyle(name.c_str(), *fontStyle);
+ return jsi::Object::createFromHostObject(
+ runtime, std::make_shared(getContext(), typeface));
+ }
+
+ JSI_HOST_FUNCTION(countFamilies) { return getObject()->countFamilies(); }
+
+ JSI_HOST_FUNCTION(getFamilyName) {
+ auto i = static_cast(arguments[0].asNumber());
+ SkString name;
+ getObject()->getFamilyName(i, &name);
+ return jsi::String::createFromUtf8(runtime, name.c_str());
+ }
+
+ JsiSkTypefaceFontProvider(std::shared_ptr context,
+ sk_sp tfProvider)
+ : JsiSkWrappingSkPtrHostObject(std::move(context),
+ std::move(tfProvider)) {}
+
+ /**
+ Returns the jsi object from a host object of this type
+ */
+ static jsi::Value toValue(jsi::Runtime &runtime,
+ std::shared_ptr context,
+ sk_sp tfProvider) {
+ return jsi::Object::createFromHostObject(
+ runtime, std::make_shared(
+ std::move(context), std::move(tfProvider)));
+ }
+};
+
+} // namespace RNSkia
diff --git a/package/cpp/api/JsiSkTypeFaceFontProviderFactory.h b/package/cpp/api/JsiSkTypeFaceFontProviderFactory.h
new file mode 100644
index 0000000000..66961bbe9c
--- /dev/null
+++ b/package/cpp/api/JsiSkTypeFaceFontProviderFactory.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "JsiSkData.h"
+#include "JsiSkHostObjects.h"
+#include "JsiSkTypefaceFontProvider.h"
+
+namespace RNSkia {
+
+namespace jsi = facebook::jsi;
+namespace para = skia::textlayout;
+
+class JsiSkTypefaceFontProviderFactory : public JsiSkHostObject {
+public:
+ JSI_HOST_FUNCTION(Make) {
+ return jsi::Object::createFromHostObject(
+ runtime, std::make_shared(
+ getContext(), sk_make_sp()));
+ }
+
+ JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkTypefaceFontProviderFactory, Make))
+
+ explicit JsiSkTypefaceFontProviderFactory(
+ std::shared_ptr context)
+ : JsiSkHostObject(std::move(context)) {}
+};
+
+} // namespace RNSkia
diff --git a/package/cpp/rnskia/RNSkPlatformContext.h b/package/cpp/rnskia/RNSkPlatformContext.h
index f7f9827d40..9ebbe41c62 100644
--- a/package/cpp/rnskia/RNSkPlatformContext.h
+++ b/package/cpp/rnskia/RNSkPlatformContext.h
@@ -15,6 +15,7 @@
#pragma clang diagnostic ignored "-Wdocumentation"
#include "SkData.h"
+#include "SkFontMgr.h"
#include "SkImage.h"
#include "SkStream.h"
#include "SkSurface.h"
@@ -132,6 +133,11 @@ class RNSkPlatformContext {
*/
virtual sk_sp makeOffscreenSurface(int width, int height) = 0;
+ /**
+ * Return the Platform specific font manager
+ */
+ virtual sk_sp createFontMgr() = 0;
+
/**
* Creates an skImage containing the screenshot of a native view and its
* children.
diff --git a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h
index 693aa3d6c3..1723ec0dac 100644
--- a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h
+++ b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h
@@ -64,6 +64,7 @@ class RNSkiOSPlatformContext : public RNSkPlatformContext {
void raiseError(const std::exception &err) override;
sk_sp makeOffscreenSurface(int width, int height) override;
+ sk_sp createFontMgr() override;
void willInvalidateModules() {
// We need to do some house-cleaning here!
diff --git a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm
index 164af865a4..52371e8329 100644
--- a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm
+++ b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm
@@ -9,8 +9,11 @@
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
+#include "SkFontMgr.h"
#include "SkSurface.h"
+#include "include/ports/SkFontMgr_mac_ct.h"
+
#pragma clang diagnostic pop
namespace RNSkia {
@@ -62,6 +65,10 @@
return SkiaMetalSurfaceFactory::makeOffscreenSurface(width, height);
}
+sk_sp RNSkiOSPlatformContext::createFontMgr() {
+ return SkFontMgr_New_CoreText(nullptr);
+}
+
void RNSkiOSPlatformContext::runOnMainThread(std::function func) {
dispatch_async(dispatch_get_main_queue(), ^{
func();
diff --git a/package/package.json b/package/package.json
index c47e3c794b..fa5f40fbe8 100644
--- a/package/package.json
+++ b/package/package.json
@@ -33,6 +33,8 @@
"libs/ios/libsvg.xcframework",
"libs/ios/libskottie.xcframework",
"libs/ios/libsksg.xcframework",
+ "libs/ios/libskparagraph.xcframework",
+ "libs/ios/libskunicode.xcframework",
"react-native-skia.podspec",
"scripts/setup-canvaskit.js",
"dist/**"
diff --git a/package/react-native-skia.podspec b/package/react-native-skia.podspec
index e3ea98cd5c..1f2c4036d2 100644
--- a/package/react-native-skia.podspec
+++ b/package/react-native-skia.podspec
@@ -34,8 +34,8 @@ Pod::Spec.new do |s|
'libs/ios/libskia.xcframework',
'libs/ios/libsvg.xcframework',
'libs/ios/libskshaper.xcframework',
- #'libs/ios/libskparagraph.xcframework',
- #'libs/ios/libskunicode.xcframework',
+ 'libs/ios/libskparagraph.xcframework',
+ 'libs/ios/libskunicode.xcframework',
]
# All iOS cpp/h files
diff --git a/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx b/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx
new file mode 100644
index 0000000000..fdea28d13e
--- /dev/null
+++ b/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx
@@ -0,0 +1,82 @@
+import { itRunsE2eOnly } from "../../../__tests__/setup";
+import { surface, testingFonts } from "../setup";
+import { FontStyle } from "../../../skia/types";
+
+describe("FontMgr", () => {
+ it("Custom font manager should work on every platform", async () => {
+ const names = await surface.eval(
+ (Skia, { fonts }) => {
+ const fontMgr = Skia.TypefaceFontProvider.Make();
+ (Object.keys(fonts) as (keyof typeof fonts)[]).flatMap((familyName) => {
+ const typefaces = fonts[familyName];
+ typefaces.forEach((typeface) => {
+ const data = Skia.Data.fromBytes(new Uint8Array(typeface));
+ fontMgr.registerFont(
+ Skia.Typeface.MakeFreeTypeFaceFromData(data)!,
+ familyName
+ );
+ });
+ });
+ return new Array(fontMgr.countFamilies())
+ .fill(0)
+ .map((_, i) => fontMgr.getFamilyName(i));
+ },
+ { fonts: testingFonts }
+ );
+ expect(names.length).toBeGreaterThan(0);
+ expect(names.indexOf("Helvetica")).toBe(-1);
+ expect(names.indexOf("Roboto")).not.toBe(-1);
+ });
+ itRunsE2eOnly("system font managers have at least one font", async () => {
+ const names = await surface.eval((Skia) => {
+ const fontMgr = Skia.FontMgr.System();
+ return new Array(fontMgr.countFamilies())
+ .fill(0)
+ .map((_, i) => fontMgr.getFamilyName(i));
+ });
+ expect(names.length).toBeGreaterThan(0);
+ if (surface.OS === "ios") {
+ expect(names.indexOf("Apple Color Emoji")).not.toBe(-1);
+ } else {
+ expect(names.indexOf("Apple Color Emoji")).toBe(-1);
+ }
+ if (surface.OS === "ios") {
+ expect(names.indexOf("Helvetica")).not.toBe(-1);
+ } else {
+ expect(names.indexOf("Helvetica")).toBe(-1);
+ }
+ });
+ itRunsE2eOnly("Non-emoji font shouldn't resolve emojis", async () => {
+ const width = await surface.eval(
+ (Skia, { fontStyle }) => {
+ const fontMgr = Skia.FontMgr.System();
+ const typeface = fontMgr.matchFamilyStyle(
+ fontMgr.getFamilyName(0),
+ fontStyle.Normal
+ );
+ const font = Skia.Font(typeface, 10);
+ return font.getGlyphIDs("😉😍");
+ },
+ { fontStyle: FontStyle }
+ );
+ expect(width).toEqual([0, 0]);
+ });
+ itRunsE2eOnly("Emoji fonts should resolve emojis", async () => {
+ const fontName =
+ surface.OS === "ios" ? "Apple Color Emoji" : "Noto Color Emoji";
+ const width = await surface.eval(
+ (Skia, { fontStyle, familyName }) => {
+ const fontMgr = Skia.FontMgr.System();
+ const typeface = fontMgr.matchFamilyStyle(familyName, fontStyle.Normal);
+ const font = Skia.Font(typeface, 10);
+ return font.getGlyphIDs("😉😍");
+ },
+ { fontStyle: FontStyle, familyName: fontName }
+ );
+ if (surface.OS === "android") {
+ expect(width).toEqual([0, 0]);
+ } else {
+ expect(width).not.toEqual([0, 0]);
+ }
+ });
+});
diff --git a/package/src/renderer/__tests__/setup.tsx b/package/src/renderer/__tests__/setup.tsx
index 341cc4bb2a..38f1027973 100644
--- a/package/src/renderer/__tests__/setup.tsx
+++ b/package/src/renderer/__tests__/setup.tsx
@@ -22,10 +22,12 @@ import { JsiDrawingContext } from "../../dom/types/DrawingContext";
jest.setTimeout(180 * 1000);
+type TestOS = "ios" | "android" | "web" | "node";
+
declare global {
var testServer: Server;
var testClient: WebSocket;
- var testOS: "ios" | "android" | "web";
+ var testOS: TestOS;
}
export let surface: TestingSurface;
const assets = new Map();
@@ -92,12 +94,24 @@ export const wait = (ms: number) =>
export const resolveFile = (uri: string) =>
fs.readFileSync(path.resolve(__dirname, `../../${uri}`));
+export const resolveFont = (uri: string) =>
+ Array.from(fs.readFileSync(path.resolve(__dirname, `../../${uri}`)));
+
(global as any).fetch = jest.fn((uri: string) =>
Promise.resolve({
arrayBuffer: () => Promise.resolve(resolveFile(uri)),
})
);
+export const testingFonts = {
+ // Noto: [resolveFont("skia/__tests__/assets/NotoSansSC-Regular.otf")],
+ Roboto: [
+ resolveFont("skia/__tests__/assets/Roboto-Regular.ttf"),
+ resolveFont("skia/__tests__/assets/Roboto-BlackItalic.ttf"),
+ ],
+ // NotoColorEmoji: [resolveFont("skia/__tests__/assets/NotoColorEmoji.ttf")],
+};
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface EmptyProps {}
@@ -274,7 +288,7 @@ interface TestingSurface {
width: number;
height: number;
fontSize: number;
- OS: string;
+ OS: TestOS;
}
class LocalSurface implements TestingSurface {
diff --git a/package/src/skia/__tests__/assets/Roboto-BlackItalic.ttf b/package/src/skia/__tests__/assets/Roboto-BlackItalic.ttf
new file mode 100644
index 0000000000..b2c6aca57b
Binary files /dev/null and b/package/src/skia/__tests__/assets/Roboto-BlackItalic.ttf differ
diff --git a/package/src/skia/__tests__/assets/Roboto-Regular.ttf b/package/src/skia/__tests__/assets/Roboto-Regular.ttf
new file mode 100644
index 0000000000..ddf4bfacb3
Binary files /dev/null and b/package/src/skia/__tests__/assets/Roboto-Regular.ttf differ
diff --git a/package/src/skia/core/Data.ts b/package/src/skia/core/Data.ts
index f7ea4c2ec8..264d4a0c90 100644
--- a/package/src/skia/core/Data.ts
+++ b/package/src/skia/core/Data.ts
@@ -18,7 +18,7 @@ const factoryWrapper = (
}
};
-const loadData = (
+export const loadData = (
source: DataSourceParam,
factory: (data: SkData) => T | null,
onError?: (err: Error) => void
@@ -37,6 +37,7 @@ const loadData = (
);
}
};
+
const useLoading = >(
source: DataSourceParam,
loader: () => Promise
@@ -61,6 +62,35 @@ const useLoading = >(
return data;
};
+export const useCollectionLoading = >(
+ source: DataSourceParam[],
+ loader: () => Promise<(T | null)[]>
+) => {
+ const mounted = useRef(false);
+ const [data, setData] = useState(null);
+ const dataRef = useRef(null);
+
+ useEffect(() => {
+ mounted.current = true;
+ loader().then((result) => {
+ const value = result.filter((r) => r !== null) as T[];
+ if (mounted.current) {
+ setData(value);
+ dataRef.current = value;
+ }
+ });
+
+ return () => {
+ dataRef.current?.forEach((instance) => instance?.dispose());
+ mounted.current = false;
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [source]);
+
+ return data;
+};
+
export const useRawData = >(
source: DataSourceParam,
factory: (data: SkData) => T | null,
diff --git a/package/src/skia/core/Font.ts b/package/src/skia/core/Font.ts
index fcf99f6ba3..c1147d8676 100644
--- a/package/src/skia/core/Font.ts
+++ b/package/src/skia/core/Font.ts
@@ -1,8 +1,11 @@
/*global SkiaApi*/
-import { useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
import { Skia } from "../Skia";
-import type { DataSourceParam } from "../types";
+import { FontSlant } from "../types";
+import type { DataModule, DataSourceParam, SkFontMgr } from "../types";
+import { Platform } from "../../Platform";
+import type { SkTypefaceFontProvider } from "../types/Paragraph/TypefaceFontProvider";
import { useTypeface } from "./Typeface";
@@ -25,3 +28,107 @@ export const useFont = (
}
}, [size, typeface]);
};
+
+type Slant = "normal" | "italic" | "oblique";
+type Weight =
+ | "normal"
+ | "bold"
+ | "100"
+ | "200"
+ | "300"
+ | "400"
+ | "500"
+ | "600"
+ | "700"
+ | "800"
+ | "900";
+
+interface RNFontStyle {
+ fontFamily: string;
+ fontSize: number;
+ fontStyle: Slant;
+ fontWeight: Weight;
+}
+
+const defaultFontStyle: RNFontStyle = {
+ fontFamily: "System",
+ fontSize: 14,
+ fontStyle: "normal",
+ fontWeight: "normal",
+};
+
+const slant = (s: Slant) => {
+ if (s === "italic") {
+ return FontSlant.Italic;
+ } else if (s === "oblique") {
+ return FontSlant.Oblique;
+ } else {
+ return FontSlant.Upright;
+ }
+};
+
+const weight = (fontWeight: Weight) => {
+ switch (fontWeight) {
+ case "normal":
+ return 400;
+ case "bold":
+ return 700;
+ default:
+ return parseInt(fontWeight, 10);
+ }
+};
+
+export const matchFont = (
+ inputStyle: Partial = {},
+ fontMgr: SkFontMgr = Skia.FontMgr.System()
+) => {
+ const fontStyle = {
+ ...defaultFontStyle,
+ ...inputStyle,
+ };
+ const style = {
+ weight: weight(fontStyle.fontWeight),
+ width: 5,
+ slant: slant(fontStyle.fontStyle),
+ };
+ const typeface = fontMgr.matchFamilyStyle(fontStyle.fontFamily, style);
+ return Skia.Font(typeface, fontStyle.fontSize);
+};
+
+export const listFontFamilies = (fontMgr: SkFontMgr = Skia.FontMgr.System()) =>
+ new Array(fontMgr.countFamilies())
+ .fill(0)
+ .map((_, i) => fontMgr.getFamilyName(i));
+
+const loadTypefaces = (typefacesToLoad: Record) => {
+ const promises = Object.keys(typefacesToLoad).flatMap((familyName) => {
+ return typefacesToLoad[familyName].map((typefaceToLoad) => {
+ return Skia.Data.fromURI(Platform.resolveAsset(typefaceToLoad)).then(
+ (data) => {
+ const tf = Skia.Typeface.MakeFreeTypeFaceFromData(data);
+ if (tf === null) {
+ throw new Error(`Couldn't create typeface for ${familyName}`);
+ }
+ return [familyName, tf] as const;
+ }
+ );
+ });
+ });
+ return Promise.all(promises);
+};
+
+export const useFonts = (sources: Record) => {
+ const [fontMgr, setFontMgr] = useState(null);
+
+ useEffect(() => {
+ loadTypefaces(sources).then((result) => {
+ const fMgr = Skia.TypefaceFontProvider.Make();
+ result.forEach(([familyName, typeface]) => {
+ fMgr.registerFont(typeface, familyName);
+ });
+ setFontMgr(fMgr);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ return fontMgr;
+};
diff --git a/package/src/skia/types/Font/FontMgr.ts b/package/src/skia/types/Font/FontMgr.ts
new file mode 100644
index 0000000000..7dce039b1a
--- /dev/null
+++ b/package/src/skia/types/Font/FontMgr.ts
@@ -0,0 +1,10 @@
+import type { SkJSIInstance } from "../JsiInstance";
+import type { SkTypeface } from "../Typeface";
+
+import type { FontStyle } from "./Font";
+
+export interface SkFontMgr extends SkJSIInstance<"FontMgr"> {
+ countFamilies(): number;
+ getFamilyName(index: number): string;
+ matchFamilyStyle(name: string, style: FontStyle): SkTypeface;
+}
diff --git a/package/src/skia/types/Font/FontMgrFactory.ts b/package/src/skia/types/Font/FontMgrFactory.ts
new file mode 100644
index 0000000000..6b05f2fa51
--- /dev/null
+++ b/package/src/skia/types/Font/FontMgrFactory.ts
@@ -0,0 +1,5 @@
+import type { SkFontMgr } from "./FontMgr";
+
+export interface FontMgrFactory {
+ System(): SkFontMgr;
+}
diff --git a/package/src/skia/types/Font/index.ts b/package/src/skia/types/Font/index.ts
index 38051b7041..24f4959506 100644
--- a/package/src/skia/types/Font/index.ts
+++ b/package/src/skia/types/Font/index.ts
@@ -1 +1,3 @@
export * from "./Font";
+export * from "./FontMgr";
+export * from "./FontMgrFactory";
diff --git a/package/src/skia/types/Paragraph/TypefaceFontProvider.ts b/package/src/skia/types/Paragraph/TypefaceFontProvider.ts
new file mode 100644
index 0000000000..9f41ac66e9
--- /dev/null
+++ b/package/src/skia/types/Paragraph/TypefaceFontProvider.ts
@@ -0,0 +1,11 @@
+import type { SkFontMgr } from "../Font";
+import type { SkTypeface } from "../Typeface";
+
+export interface SkTypefaceFontProvider extends SkFontMgr {
+ /**
+ * Registers a given typeface with the given family name.
+ * @param typeface - Typeface.
+ * @param family
+ */
+ registerFont(typeface: SkTypeface, familyName: string): void;
+}
diff --git a/package/src/skia/types/Paragraph/TypefaceFontProviderFactory.ts b/package/src/skia/types/Paragraph/TypefaceFontProviderFactory.ts
new file mode 100644
index 0000000000..3eb127b509
--- /dev/null
+++ b/package/src/skia/types/Paragraph/TypefaceFontProviderFactory.ts
@@ -0,0 +1,5 @@
+import type { SkTypefaceFontProvider } from "./TypefaceFontProvider";
+
+export interface TypefaceFontProviderFactory {
+ Make(): SkTypefaceFontProvider;
+}
diff --git a/package/src/skia/types/Skia.ts b/package/src/skia/types/Skia.ts
index 8d13c03146..e08a1ec034 100644
--- a/package/src/skia/types/Skia.ts
+++ b/package/src/skia/types/Skia.ts
@@ -1,7 +1,7 @@
import type { ImageFilterFactory } from "./ImageFilter";
import type { PathFactory } from "./Path";
import type { ColorFilterFactory } from "./ColorFilter";
-import type { SkFont } from "./Font";
+import type { SkFont, FontMgrFactory } from "./Font";
import type { SkTypeface, TypefaceFactory } from "./Typeface";
import type { ImageFactory } from "./Image";
import type { MaskFilterFactory } from "./MaskFilter";
@@ -27,7 +27,7 @@ import type { SkPath } from "./Path/Path";
import type { SkContourMeasureIter } from "./ContourMeasure";
import type { PictureFactory, SkPictureRecorder } from "./Picture";
import type { Color, SkColor } from "./Color";
-
+import type { TypefaceFontProviderFactory } from "./Paragraph/TypefaceFontProviderFactory";
/**
* Declares the interface for the native Skia API
*/
@@ -51,6 +51,8 @@ export interface Skia {
ColorFilter: ColorFilterFactory;
Font: (typeface?: SkTypeface, size?: number) => SkFont;
Typeface: TypefaceFactory;
+ TypefaceFontProvider: TypefaceFontProviderFactory;
+ FontMgr: FontMgrFactory;
MaskFilter: MaskFilterFactory;
RuntimeEffect: RuntimeEffectFactory;
ImageFilter: ImageFilterFactory;
diff --git a/package/src/skia/web/Host.ts b/package/src/skia/web/Host.ts
index 3306183748..d2f118f554 100644
--- a/package/src/skia/web/Host.ts
+++ b/package/src/skia/web/Host.ts
@@ -29,7 +29,7 @@ export abstract class BaseHostObject
this.__typename__ = typename;
}
- abstract dispose: () => void;
+ abstract dispose(): void;
}
export abstract class HostObject extends BaseHostObject<
diff --git a/package/src/skia/web/JsiSkFontMgr.ts b/package/src/skia/web/JsiSkFontMgr.ts
new file mode 100644
index 0000000000..a316ae7ad8
--- /dev/null
+++ b/package/src/skia/web/JsiSkFontMgr.ts
@@ -0,0 +1,26 @@
+import type { CanvasKit, FontMgr } from "canvaskit-wasm";
+
+import type { FontStyle, SkFontMgr, SkTypeface } from "../types";
+
+import { HostObject, NotImplementedOnRNWeb } from "./Host";
+
+export class JsiSkFontMgr
+ extends HostObject
+ implements SkFontMgr
+{
+ constructor(CanvasKit: CanvasKit, ref: FontMgr) {
+ super(CanvasKit, ref, "FontMgr");
+ }
+ dispose() {
+ this.ref.delete();
+ }
+ countFamilies() {
+ return this.ref.countFamilies();
+ }
+ getFamilyName(index: number) {
+ return this.ref.getFamilyName(index);
+ }
+ matchFamilyStyle(_familyName: string, _fontStyle: FontStyle): SkTypeface {
+ throw new NotImplementedOnRNWeb();
+ }
+}
diff --git a/package/src/skia/web/JsiSkFontMgrFactory.ts b/package/src/skia/web/JsiSkFontMgrFactory.ts
new file mode 100644
index 0000000000..e07447d8ab
--- /dev/null
+++ b/package/src/skia/web/JsiSkFontMgrFactory.ts
@@ -0,0 +1,22 @@
+import type { CanvasKit } from "canvaskit-wasm";
+
+import type { FontMgrFactory } from "../types";
+
+import { Host } from "./Host";
+import { JsiSkFontMgr } from "./JsiSkFontMgr";
+
+export class JsiSkFontMgrFactory extends Host implements FontMgrFactory {
+ constructor(CanvasKit: CanvasKit) {
+ super(CanvasKit);
+ }
+
+ System() {
+ const fontMgr = this.CanvasKit.TypefaceFontProvider.Make();
+ if (!fontMgr) {
+ throw new Error("Couldn't create system font manager");
+ }
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ return new JsiSkFontMgr(this.CanvasKit, fontMgr);
+ }
+}
diff --git a/package/src/skia/web/JsiSkTypefaceFontProvider.ts b/package/src/skia/web/JsiSkTypefaceFontProvider.ts
new file mode 100644
index 0000000000..47d3515d40
--- /dev/null
+++ b/package/src/skia/web/JsiSkTypefaceFontProvider.ts
@@ -0,0 +1,89 @@
+import type { CanvasKit, TypefaceFontProvider } from "canvaskit-wasm";
+
+import type { SkTypefaceFontProvider } from "../types/Paragraph/TypefaceFontProvider";
+import type { FontStyle, SkTypeface } from "../types";
+
+import { HostObject, NotImplementedOnRNWeb } from "./Host";
+
+export class JsiSkTypefaceFontProvider
+ extends HostObject
+ implements SkTypefaceFontProvider
+{
+ private allocatedPointers: number[] = [];
+
+ constructor(CanvasKit: CanvasKit, ref: TypefaceFontProvider) {
+ super(CanvasKit, ref, "FontMgr");
+ }
+
+ matchFamilyStyle(_name: string, _style: FontStyle): SkTypeface {
+ throw new NotImplementedOnRNWeb();
+ }
+ countFamilies() {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ return this.ref.countFamilies();
+ }
+ getFamilyName(index: number) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ return this.ref.getFamilyName(index);
+ }
+ registerFont(typeface: SkTypeface, familyName: string) {
+ const strLen = lengthBytesUTF8(familyName) + 1;
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ const strPtr = this.CanvasKit._malloc(strLen);
+ stringToUTF8(this.CanvasKit, familyName, strPtr, strLen);
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ this.ref._registerFont(typeface.ref, strPtr);
+ }
+
+ dispose() {
+ for (const ptr of this.allocatedPointers) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ this.CanvasKit._free(ptr);
+ }
+
+ this.ref.delete();
+ }
+}
+
+const lengthBytesUTF8 = (str: string) => {
+ // TextEncoder will give us the byte length in UTF8 form
+ const encoder = new TextEncoder();
+ const utf8 = encoder.encode(str);
+ return utf8.length;
+};
+
+const stringToUTF8 = (
+ CanvasKit: CanvasKit,
+ str: string,
+ outPtr: number,
+ maxBytesToWrite: number
+) => {
+ // TextEncoder will give us the byte array in UTF8 form
+ const encoder = new TextEncoder();
+ const utf8 = encoder.encode(str);
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ const heap: Int8Array = CanvasKit.HEAPU8;
+
+ // Check if there's enough space
+ if (utf8.length > maxBytesToWrite) {
+ throw new Error("Not enough space to write UTF8 encoded string");
+ }
+
+ // Copy the bytes
+ for (let i = 0; i < utf8.length; i++) {
+ heap[outPtr + i] = utf8[i];
+ }
+
+ // Null terminate
+ if (utf8.length < maxBytesToWrite) {
+ heap[outPtr + utf8.length] = 0;
+ }
+};
diff --git a/package/src/skia/web/JsiSkTypefaceFontProviderFactory.ts b/package/src/skia/web/JsiSkTypefaceFontProviderFactory.ts
new file mode 100644
index 0000000000..e940c18498
--- /dev/null
+++ b/package/src/skia/web/JsiSkTypefaceFontProviderFactory.ts
@@ -0,0 +1,18 @@
+import type { CanvasKit } from "canvaskit-wasm";
+
+import { Host } from "./Host";
+import { JsiSkTypefaceFontProvider } from "./JsiSkTypefaceFontProvider";
+
+export class JsiSkTypefaceFontProviderFactory
+ extends Host
+ implements JsiSkTypefaceFontProviderFactory
+{
+ constructor(CanvasKit: CanvasKit) {
+ super(CanvasKit);
+ }
+
+ Make() {
+ const tf = this.CanvasKit.TypefaceFontProvider.Make();
+ return new JsiSkTypefaceFontProvider(this.CanvasKit, tf);
+ }
+}
diff --git a/package/src/skia/web/JsiSkia.ts b/package/src/skia/web/JsiSkia.ts
index 500f891a75..88909401fe 100644
--- a/package/src/skia/web/JsiSkia.ts
+++ b/package/src/skia/web/JsiSkia.ts
@@ -37,6 +37,8 @@ import { JsiSkFont } from "./JsiSkFont";
import { MakeVertices } from "./JsiSkVerticesFactory";
import { JsiSkPath } from "./JsiSkPath";
import { JsiSkTypeface } from "./JsiSkTypeface";
+import { JsiSkTypefaceFontProviderFactory } from "./JsiSkTypefaceFontProviderFactory";
+import { JsiSkFontMgrFactory } from "./JsiSkFontMgrFactory";
export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({
Point: (x: number, y: number) =>
@@ -102,4 +104,6 @@ export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({
return new JsiSkRect(CanvasKit, CanvasKit.XYWHRect(x, y, width, height));
},
Surface: new JsiSkSurfaceFactory(CanvasKit),
+ TypefaceFontProvider: new JsiSkTypefaceFontProviderFactory(CanvasKit),
+ FontMgr: new JsiSkFontMgrFactory(CanvasKit),
});
diff --git a/scripts/build-npm-package.ts b/scripts/build-npm-package.ts
index 3d8b4bd139..fa0d6a1580 100644
--- a/scripts/build-npm-package.ts
+++ b/scripts/build-npm-package.ts
@@ -44,8 +44,8 @@ if (process.env.GITHUB_RUN_NUMBER === undefined) {
"libsvg.a",
"libskottie.a",
"libsksg.a",
- //"libskparagraph.a",
- //"libskunicode.a",
+ "libskparagraph.a",
+ "libskunicode.a",
].forEach((target) => {
const path = `./package/libs/android/${cpu}/${target}`;
checkFileExists(
@@ -63,8 +63,8 @@ if (process.env.GITHUB_RUN_NUMBER === undefined) {
"libsvg.xcframework",
"libskottie.xcframework",
"libsksg.xcframework",
- // "libskparagraph.xcframework",
- // "libskunicode.xcframework",
+ "libskparagraph.xcframework",
+ "libskunicode.xcframework",
].forEach((lib) => {
checkFileExists(
`./package/libs/ios/${lib}`,
diff --git a/scripts/skia-configuration.ts b/scripts/skia-configuration.ts
index bac816d5ed..3eccd6cfeb 100644
--- a/scripts/skia-configuration.ts
+++ b/scripts/skia-configuration.ts
@@ -2,47 +2,32 @@ import { executeCmdSync } from "./utils";
const NdkDir: string = process.env.ANDROID_NDK ?? "";
-// To build with the paragraph API's, you need to set this to true, and
-// you need to update the following files with some uncommenting:
-// 1) CMakeLists.txt
-// 2) react-native-skia.podspec
-// 3) package.json - add the following files to the files array:
-// "libs/ios/libskparagraph.xcframework",
-// "libs/ios/libskunicode.xcframework",
-// 4) build-skia.yml:
-// Line 60:
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libskparagraph.a
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm/libskunicode.a
-// Line 72:
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libskparagraph.a
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/arm64/libskunicode.a
-// Line 84:
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libskparagraph.a
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x86/libskunicode.a
-// Line 96:
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskparagraph.a
-// ${{ env.WORKING_DIRECTORY }}/externals/skia/out/android/x64/libskunicode.a
-// Line 108:
-// ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskparagraph.xcframework
-// ${{ env.WORKING_DIRECTORY }}/package/libs/ios/libskunicode.xcframework
-// 5) build-npm-package.ts:
-// Line 47-48, uncomment
-// Line 66-67, uncomment
-// 6) Workflow-copy-libs.ts:
-// 27-28 and 36-37, uncomment
-export const BUILD_WITH_PARAGRAPH = false;
-const ParagraphArgs = BUILD_WITH_PARAGRAPH
- ? [
- ["skia_enable_paragraph", true],
- ["skia_use_icu", true],
- ["skia_use_system_icu", false],
- ["skia_use_harfbuzz", true],
- ["skia_use_system_harfbuzz", false],
- ]
- : [
- ["skia_use_harfbuzz", false],
- ["skia_use_icu", false],
- ];
+export const BUILD_WITH_PARAGRAPH = true;
+const NoParagraphArgs = [
+ ["skia_use_harfbuzz", false],
+ ["skia_use_icu", false],
+];
+
+// To build the paragraph API:
+// On Android: we use system ICU
+// On iOS: we use neither system nor client ICU
+const CommonParagraphArgs = [
+ ["skia_enable_paragraph", true],
+ ["skia_use_system_icu", false],
+ ["skia_use_harfbuzz", true],
+ ["skia_use_system_harfbuzz", false],
+];
+const ParagraphArgsAndroid = BUILD_WITH_PARAGRAPH ? [
+ ...CommonParagraphArgs,
+ ["skia_use_icu", true],
+ ["skia_use_runtime_icu", true],
+] : NoParagraphArgs;
+
+const ParagraphArgsIOS = BUILD_WITH_PARAGRAPH ? [
+ ...CommonParagraphArgs,
+ ["skia_use_icu", false],
+ ["skia_use_client_icu", true],
+] : NoParagraphArgs;
const ParagraphOutputs = BUILD_WITH_PARAGRAPH
? ["libskparagraph.a", "libskunicode.a"]
@@ -64,7 +49,6 @@ export const commonArgs = [
["skia_enable_flutter_defines", true],
["paragraph_tests_enabled", false],
["is_component_build", false],
- ...ParagraphArgs,
];
export type PlatformName = "ios" | "android";
@@ -108,7 +92,6 @@ export const configurations: Configuration = {
args: [
["ndk", `"${NdkDir}"`],
["skia_use_system_freetype2", false],
- ["skia_use_runtime_icu", true],
["skia_use_gl", true],
["cc", '"clang"'],
["cxx", '"clang++"'],
@@ -116,6 +99,7 @@ export const configurations: Configuration = {
"extra_cflags",
'["-DSKIA_C_DLL", "-DHAVE_SYSCALL_GETRANDOM", "-DXML_DEV_URANDOM"]',
],
+ ...ParagraphArgsAndroid,
],
outputRoot: "package/libs/android",
outputNames: [
@@ -161,6 +145,7 @@ export const configurations: Configuration = {
["skia_use_metal", true],
["cc", '"clang"'],
["cxx", '"clang++"'],
+ ...ParagraphArgsIOS
],
outputRoot: "package/libs/ios",
outputNames: [
diff --git a/scripts/workflow-copy-libs.ts b/scripts/workflow-copy-libs.ts
index d545a1ca49..da28a3f04c 100644
--- a/scripts/workflow-copy-libs.ts
+++ b/scripts/workflow-copy-libs.ts
@@ -24,8 +24,8 @@ const androidFiles = [
"libsvg.a",
"libskottie.a",
"libsksg.a",
- // "libskparagraph.a",
- // "libskunicode.a",
+ "libskparagraph.a",
+ "libskunicode.a",
];
const iosFiles = [
"libskia.xcframework",
@@ -33,8 +33,8 @@ const iosFiles = [
"libsvg.xcframework",
"libskottie.xcframework",
"libsksg.xcframework",
- // "libskparagraph.xcframework",
- // "libskunicode.xcframework",
+ "libskparagraph.xcframework",
+ "libskunicode.xcframework",
];
const copyFiles = (from: string, to: string, files: string[]) => {