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 ( + + + + + {familyNames.map((fontFamily, i) => { + const font = matchFont({ fontFamily }); + const resolvedFont = + font.getGlyphIDs(fontFamily)[0] === 0 ? subtitleFont : font; + return ( + + ); + })} + + {customfamilyNames.map((fontFamily, i) => { + const font = matchFont({ fontFamily }, customFontMgr); + + const resolvedFont = + font.getGlyphIDs(fontFamily)[0] === 0 ? subtitleFont : font; + 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[]) => {