diff --git a/docs/docs/text/fonts.md b/docs/docs/text/fonts.md index f0153cecb1..b8178b972a 100644 --- a/docs/docs/text/fonts.md +++ b/docs/docs/text/fonts.md @@ -8,6 +8,19 @@ 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. +The `matchFont()` function will always return a font. +So a fast way to display text in React Native Skia would be the following: + +```tsx twoslash +import {Text, matchFont} from "@shopify/react-native-skia"; + +const Demo = () => { + return ( + + ); +}; +``` + ## Custom Fonts The `useFonts` hooks allows you to load custom fonts to be used for your Skia drawing. @@ -121,11 +134,15 @@ The `fontStyle` object can have the following list of optional attributes: 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"), -]); +```jsx twoslash +import {matchFont, useFonts} from "@shopify/react-native-skia"; + +const fontMgr = useFonts({ + Roboto: [ + require("../../Tests/assets/Roboto-Medium.ttf"), + require("../../Tests/assets/Roboto-Bold.ttf"), + ] +}); const font = matchFont(fontStyle, fontMgr); ``` diff --git a/example/src/Examples/API/FontMgr.tsx b/example/src/Examples/API/FontMgr.tsx index 1460721db9..3e937c7e97 100644 --- a/example/src/Examples/API/FontMgr.tsx +++ b/example/src/Examples/API/FontMgr.tsx @@ -70,8 +70,7 @@ export const FontMgr = () => { })} {customfamilyNames.map((fontFamily, i) => { - const font = matchFont({ fontFamily }, customFontMgr); - + const font = matchFont({ fontFamily, fontSize: 16 }, customFontMgr); const resolvedFont = font.getGlyphIDs(fontFamily)[0] === 0 ? subtitleFont : font; return ( @@ -84,6 +83,12 @@ export const FontMgr = () => { /> ); })} + ); diff --git a/package/cpp/api/JsiSkFontMgr.h b/package/cpp/api/JsiSkFontMgr.h index 5df7a1a224..5b5561ca8c 100644 --- a/package/cpp/api/JsiSkFontMgr.h +++ b/package/cpp/api/JsiSkFontMgr.h @@ -51,4 +51,4 @@ class JsiSkFontMgr : public JsiSkWrappingSkPtrHostObject { JSI_EXPORT_FUNC(JsiSkFontMgr, matchFamilyStyle)) }; -} // namespace RNSkia \ No newline at end of file +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkFontStyle.h b/package/cpp/api/JsiSkFontStyle.h index 961ea60d79..ba45018627 100644 --- a/package/cpp/api/JsiSkFontStyle.h +++ b/package/cpp/api/JsiSkFontStyle.h @@ -36,12 +36,18 @@ class JsiSkFontStyle : public JsiSkWrappingSharedPtrHostObject { 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 weightProp = object.getProperty(runtime, "weight"); + auto weight = static_cast(weightProp.isUndefined() + ? SkFontStyle::Weight::kNormal_Weight + : weightProp.asNumber()); + auto widthProp = object.getProperty(runtime, "width"); + auto width = static_cast(widthProp.isUndefined() + ? SkFontStyle::Width::kNormal_Width + : widthProp.asNumber()); + auto slantProp = object.getProperty(runtime, "slant"); auto slant = static_cast( - object.getProperty(runtime, "slant").asNumber()); + slantProp.isUndefined() ? SkFontStyle::Slant::kUpright_Slant + : slantProp.asNumber()); SkFontStyle style(weight, width, slant); return std::make_shared(style); } diff --git a/package/cpp/api/JsiSkTypeFaceFontProvider.h b/package/cpp/api/JsiSkTypeFaceFontProvider.h index e559a2e44c..11be71c4d6 100644 --- a/package/cpp/api/JsiSkTypeFaceFontProvider.h +++ b/package/cpp/api/JsiSkTypeFaceFontProvider.h @@ -27,7 +27,8 @@ namespace para = skia::textlayout; class JsiSkTypefaceFontProvider : public JsiSkWrappingSkPtrHostObject { public: - EXPORT_JSI_API_TYPENAME(JsiSkTypefaceFontProvider, FontMgr) + EXPORT_JSI_API_TYPENAME(JsiSkTypefaceFontProvider, TypefaceFontProvider) + JSI_EXPORT_FUNCTIONS( JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, dispose), JSI_EXPORT_FUNC(JsiSkTypefaceFontProvider, registerFont), @@ -46,7 +47,8 @@ class JsiSkTypefaceFontProvider 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); + sk_sp set(getObject()->onMatchFamily(name.c_str())); + sk_sp typeface(set->matchStyle(*fontStyle)); return jsi::Object::createFromHostObject( runtime, std::make_shared(getContext(), typeface)); } diff --git a/package/cpp/api/JsiSkTypeface.h b/package/cpp/api/JsiSkTypeface.h index 3f76df5c8f..27d3401dfd 100644 --- a/package/cpp/api/JsiSkTypeface.h +++ b/package/cpp/api/JsiSkTypeface.h @@ -2,6 +2,7 @@ #include #include +#include #include @@ -23,12 +24,33 @@ namespace jsi = facebook::jsi; class JsiSkTypeface : public JsiSkWrappingSkPtrHostObject { public: EXPORT_JSI_API_TYPENAME(JsiSkTypeface, Typeface) - JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkTypeface, dispose)) + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkTypeface, getGlyphIDs), + JSI_EXPORT_FUNC(JsiSkTypeface, dispose)) JsiSkTypeface(std::shared_ptr context, sk_sp typeface) : JsiSkWrappingSkPtrHostObject(std::move(context), std::move(typeface)) {} + JSI_HOST_FUNCTION(getGlyphIDs) { + auto str = arguments[0].asString(runtime).utf8(runtime); + int numGlyphIDs = + count > 1 && !arguments[1].isNull() && !arguments[1].isUndefined() + ? static_cast(arguments[1].asNumber()) + : getObject()->textToGlyphs(str.c_str(), str.length(), + SkTextEncoding::kUTF8, nullptr, 0); + std::vector glyphIDs; + glyphIDs.resize(numGlyphIDs); + getObject()->textToGlyphs(str.c_str(), str.length(), SkTextEncoding::kUTF8, + static_cast(glyphIDs.data()), + numGlyphIDs); + auto jsiGlyphIDs = jsi::Array(runtime, numGlyphIDs); + for (int i = 0; i < numGlyphIDs; i++) { + jsiGlyphIDs.setValueAtIndex(runtime, i, + jsi::Value(static_cast(glyphIDs[i]))); + } + return jsiGlyphIDs; + } + /** Returns the jsi object from a host object of this type */ diff --git a/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx b/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx index fdea28d13e..57a21622af 100644 --- a/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx +++ b/package/src/renderer/__tests__/e2e/FontMgr.spec.tsx @@ -2,31 +2,53 @@ import { itRunsE2eOnly } from "../../../__tests__/setup"; import { surface, testingFonts } from "../setup"; import { FontStyle } from "../../../skia/types"; +// Currently this tests only run on iOS and Android +// We've contributed a patch to CanvasKit to make it work on Web too in the next release: +// https://github.com/google/skia/blob/main/modules/canvaskit/CHANGELOG.md#unreleased 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( + "Custom font manager should work on every platform", + async () => { + const result = 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 + ); + }); + } + ); + const familyNames = new Array(fontMgr.countFamilies()) + .fill(0) + .map((_, i) => fontMgr.getFamilyName(i)); + + const identities = new Array(fontMgr.countFamilies()) + .fill(0) + .map((_, i) => { + const t1 = fontMgr.matchFamilyStyle(fontMgr.getFamilyName(i), { + weight: 400, + }); + const t2 = fontMgr.matchFamilyStyle(fontMgr.getFamilyName(i), { + weight: 900, + }); + return t1 !== t2; + }); + return { familyNames, identities }; + }, + { fonts: testingFonts } + ); + expect(result.identities).toEqual([true]); + expect(result.familyNames.length).toBeGreaterThan(0); + expect(result.familyNames.indexOf("Helvetica")).toBe(-1); + expect(result.familyNames.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(); @@ -79,4 +101,7 @@ describe("FontMgr", () => { expect(width).not.toEqual([0, 0]); } }); + // Add test + // * Passing |nullptr| as the parameter for |familyName| will return the + // * default system font. }); diff --git a/package/src/skia/types/Font/FontMgr.ts b/package/src/skia/types/Font/FontMgr.ts index 7dce039b1a..e4bda405ef 100644 --- a/package/src/skia/types/Font/FontMgr.ts +++ b/package/src/skia/types/Font/FontMgr.ts @@ -6,5 +6,5 @@ import type { FontStyle } from "./Font"; export interface SkFontMgr extends SkJSIInstance<"FontMgr"> { countFamilies(): number; getFamilyName(index: number): string; - matchFamilyStyle(name: string, style: FontStyle): SkTypeface; + matchFamilyStyle(name?: string, style?: FontStyle): SkTypeface; } diff --git a/package/src/skia/types/Typeface/Typeface.ts b/package/src/skia/types/Typeface/Typeface.ts index 5bcbaedc9b..072374cb15 100644 --- a/package/src/skia/types/Typeface/Typeface.ts +++ b/package/src/skia/types/Typeface/Typeface.ts @@ -1,3 +1,12 @@ import type { SkJSIInstance } from "../JsiInstance"; -export type SkTypeface = SkJSIInstance<"Typeface">; +export interface SkTypeface extends SkJSIInstance<"Typeface"> { + /** + * Retrieves the glyph ids for each code point in the provided string. This call is passed to + * the typeface of this font. Note that glyph IDs are typeface-dependent; different faces + * may have different ids for the same code point. + * @param str + * @param numCodePoints - the number of code points in the string. Defaults to str.length. + */ + getGlyphIDs(str: string, numCodePoints?: number): number[]; +} diff --git a/package/src/skia/web/JsiSkTypeface.ts b/package/src/skia/web/JsiSkTypeface.ts index c180204105..56b91f9d1b 100644 --- a/package/src/skia/web/JsiSkTypeface.ts +++ b/package/src/skia/web/JsiSkTypeface.ts @@ -26,6 +26,10 @@ export class JsiSkTypeface return false; } + getGlyphIDs(str: string, numCodePoints?: number) { + return Array.from(this.ref.getGlyphIDs(str, numCodePoints)); + } + dispose = () => { this.ref.delete(); };