Skip to content

Commit

Permalink
Add API to configure the fallback fonts provider when all default met…
Browse files Browse the repository at this point in the history
…hods return null (#935)

New API:

```kotlin
fun TypefaceFontProvider.createAsFallbackProvider(): TypefaceFontProvider
fun FontMgr.defaultWithFallbackFontProvider(fallback: TypefaceFontProvider): FontMgr
```

Usage:

```kotlin
val fontCollection = FontCollection()

val notoEmojiTypeface = Typeface.makeFromData(Data.makeFromBytes(notoEmojisBytes))
val fallbackProvider = TypefaceFontProvider.createAsFallbackProvider().apply {
    registerTypeface(notoEmojiTypeface)
}

fontCollection.setDefaultFontManager(FontMgr.defaultWithFallbackFontProvider(fallbackProvider))
```

<img width="400" alt="Screenshot 2024-06-07 at 14 48 28"
src="https://github.com/JetBrains/skiko/assets/7372778/76dc1330-d8e2-4be7-ad92-68214b058b4f">
  • Loading branch information
eymar authored Jun 19, 2024
1 parent a03d0a5 commit 45c1a7d
Show file tree
Hide file tree
Showing 21 changed files with 495 additions and 19 deletions.
1 change: 1 addition & 0 deletions samples/SkiaMultiplatformSample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ kotlin {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.skiko:skiko:$version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}

Expand Down
2 changes: 1 addition & 1 deletion samples/SkiaMultiplatformSample/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kotlin.code.style=official
org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=512m
kotlin.version=1.9.21
#skiko.composite.build=1
skiko.composite.build=1
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import javax.swing.*

fun main() {
val skiaLayer = SkiaLayer()
val clocks = AwtClocks(skiaLayer)
skiaLayer.renderDelegate = SkiaLayerRenderDelegate(skiaLayer, clocks)
val app = run {
//EmojiStory()
AwtClocks(skiaLayer)
}
skiaLayer.renderDelegate = SkiaLayerRenderDelegate(skiaLayer, app)
SwingUtilities.invokeLater {
val window = JFrame("Skiko example").apply {
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.jetbrains.skiko.sample

import org.jetbrains.skia.Canvas
import org.jetbrains.skia.FontMgr
import org.jetbrains.skia.paragraph.FontCollection
import org.jetbrains.skia.paragraph.ParagraphBuilder
import org.jetbrains.skia.paragraph.ParagraphStyle
import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.SkikoRenderDelegate
import org.jetbrains.skiko.hostOs

class EmojiStory : SkikoRenderDelegate {

private val platformYOffset = if (hostOs == OS.Ios) 50f else 5f

private val style = ParagraphStyle().apply {
textStyle = TextStyle().apply {
this.fontSize = 16.0f
}.setColor(0xFF000000.toInt())
}

private val paragraph = ParagraphBuilder(style, fontCollection)
.addText("\uD83D\uDCE1 Antenna - 天线\n")
.addText("\uFE0F\uFE0F\uFE0F\n")
.addText("\uD83D\uDE32 \uD83E\uDD14 \uD83D\uDCA1\n")
.addText("\uD83C\uDF0D Earth - 地球\n")
.addText("\uD83D\uDE80 Space rocket - 太空火箭\n")
.addText("\uD83C\uDF14 Moon - 月亮\n")
.addText("\uD83D\uDCAB Stars - 星星\n")
.addText("\uD83C\uDF0C Galaxy - 星系\n")
.addText("\uD83D\uDEF8 UFO - 飞碟\n")
.addText("\uD83D\uDC7D Alien - 外星人\n")
.addText("\uD83D\uDC4B \uD83E\uDD1D \uD83C\uDF7B \n")
.build()

override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
paragraph.layout(Float.POSITIVE_INFINITY)
paragraph.paint(canvas, 5f, platformYOffset)
canvas.resetMatrix()
}

companion object {
val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.default)
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
package org.jetbrains.skiko.sample

import kotlinx.browser.document
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.jetbrains.skia.Data
import org.jetbrains.skia.FontMgrWithFallback
import org.jetbrains.skia.Typeface
import org.jetbrains.skia.paragraph.TypefaceFontProviderWithFallback
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerRenderDelegate
import org.w3c.dom.HTMLCanvasElement


fun main() {
runApp()
//runClocksApp()
runEmojiStoryApp()
}

internal fun runApp() {
internal fun runClocksApp() {
val canvas = document.getElementById("SkikoTarget") as HTMLCanvasElement
canvas.setAttribute("tabindex", "0")
val skiaLayer = SkiaLayer()
val clocks = WebClocks(skiaLayer, canvas)
skiaLayer.renderDelegate = SkiaLayerRenderDelegate(skiaLayer, clocks)
skiaLayer.needRedraw()
}

private val notoColorEmoji = "https://storage.googleapis.com/skia-cdn/misc/NotoColorEmoji.ttf"
private val notoSancSC = "./NotoSansSC-Regular.ttf"

internal fun runEmojiStoryApp() {
val canvas = document.getElementById("SkikoTarget") as HTMLCanvasElement
canvas.setAttribute("tabindex", "0")
val skiaLayer = SkiaLayer()
val app = EmojiStory()
skiaLayer.renderDelegate = SkiaLayerRenderDelegate(skiaLayer, app)
skiaLayer.attachTo(canvas)

MainScope().launch {
val notoEmojisBytes = loadRes(notoColorEmoji).toByteArray()
val notoSansSCBytes = loadRes(notoSancSC).toByteArray()
val notoEmojiTypeface = Typeface.makeFromData(Data.makeFromBytes(notoEmojisBytes))
val notoSansSCTypeface = Typeface.makeFromData(Data.makeFromBytes(notoSansSCBytes))

val tfp = TypefaceFontProviderWithFallback().apply {
registerTypeface(notoEmojiTypeface)
registerTypeface(notoSansSCTypeface)
}
EmojiStory.fontCollection.setDefaultFontManager(FontMgrWithFallback(tfp))

skiaLayer.needRedraw()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.jetbrains.skiko.sample

import kotlinx.browser.window
import kotlinx.coroutines.await
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.Int8Array
import org.w3c.fetch.Response
import org.w3c.xhr.XMLHttpRequest
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
import kotlin.wasm.unsafe.withScopedMemoryAllocator

suspend fun loadRes(url: String): ArrayBuffer {
return window.fetch(url).await<Response>().arrayBuffer().await()
}

private class MissingResourceException(url: String): Exception("GET $url failed")

fun ArrayBuffer.toByteArray(): ByteArray {
val source = Int8Array(this, 0, byteLength)
return jsInt8ArrayToKotlinByteArray(source)
}

@JsFun(
""" (src, size, dstAddr) => {
const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
mem8.set(src);
}
"""
)
internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)

internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
val size = x.length

@OptIn(UnsafeWasmMemoryApi::class)
return withScopedMemoryAllocator { allocator ->
val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
jsExportInt8ArrayToWasm(x, size, dstAddress)
ByteArray(size) { i -> (memBuffer + i).loadByte() }
}
}
Binary file not shown.
85 changes: 85 additions & 0 deletions skiko/src/commonMain/cpp/common/FontMgrWithFallbackWrapper.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "FontMgrWithFallbackWrapper.hh"

FontMgrWithFallbackWrapper::FontMgrWithFallbackWrapper(sk_sp<TypefaceFontProviderWithFallback> fallbackTypefaceFontProvider)
: fallbackFontProvider(std::move(fallbackTypefaceFontProvider)) {
wrappedFntMgr = SkFontMgr::RefDefault();
}

int FontMgrWithFallbackWrapper::onCountFamilies() const {
return wrappedFntMgr->countFamilies();
}

void FontMgrWithFallbackWrapper::onGetFamilyName(int index, SkString* familyName) const {
wrappedFntMgr->getFamilyName(index, familyName);
}

sk_sp<SkFontStyleSet> FontMgrWithFallbackWrapper::onMatchFamily(const char familyName[]) const {
return wrappedFntMgr->matchFamily(familyName);
}

sk_sp<SkFontStyleSet> FontMgrWithFallbackWrapper::onCreateStyleSet(int ix) const {
return wrappedFntMgr->createStyleSet(ix);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMatchFamilyStyle(const char familyName[], const SkFontStyle& style) const {
return wrappedFntMgr->matchFamilyStyle(familyName, style);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMatchFamilyStyleCharacter(
const char fontFamily[], const SkFontStyle& style,
const char* bcp47[], int bcp47Count,
SkUnichar character
) const {
auto typeface = wrappedFntMgr->matchFamilyStyleCharacter(fontFamily, style, bcp47, bcp47Count, character);
if (!typeface) {
typeface = fallbackFontProvider->fallbackForChar(character);
}
return typeface;
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMakeFromData(sk_sp<SkData> data, int ix) const {
return wrappedFntMgr->makeFromData(std::move(data), ix);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset> stream, int ix) const {
return wrappedFntMgr->makeFromStream(std::move(stream), ix);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset> stream, const SkFontArguments& args) const {
return wrappedFntMgr->makeFromStream(std::move(stream), args);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onMakeFromFile(const char path[], int ix) const {
return wrappedFntMgr->makeFromFile(path, ix);
}

sk_sp<SkTypeface> FontMgrWithFallbackWrapper::onLegacyMakeTypeface(const char name[], SkFontStyle style) const {
return wrappedFntMgr->legacyMakeTypeface(name, style);
}

void FontMgrWithFallbackWrapper::setFallbackFontProvider(sk_sp<TypefaceFontProviderWithFallback> fontProvider) {
fallbackFontProvider = std::move(fontProvider);
}

/* TypefaceFontProviderWithFallback section */

size_t TypefaceFontProviderWithFallback::registerTypeface(sk_sp<SkTypeface> typeface) {
registeredTypefaces.push_back(typeface);
return TypefaceFontProvider::registerTypeface(std::move(typeface));
}

size_t TypefaceFontProviderWithFallback::registerTypeface(sk_sp<SkTypeface> typeface, const SkString& alias) {
registeredTypefaces.push_back(typeface);
return TypefaceFontProvider::registerTypeface(std::move(typeface), alias);
}

sk_sp<SkTypeface> TypefaceFontProviderWithFallback::fallbackForChar(SkUnichar character) const {
for (const auto& typeface : registeredTypefaces) {
if (!typeface) continue;
auto glyph = typeface->unicharToGlyph(character);
if (glyph != 0) {
return typeface;
}
}
return nullptr;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#ifndef FONT_MGR_WRAPPER_H
#define FONT_MGR_WRAPPER_H

#include "SkData.h"
#include "SkStream.h"
#include "SkTypeface.h"
#include "SkFontMgr.h"
#include "TypefaceFontProvider.h"

using namespace skia::textlayout;

// extends the default TypefaceFontProvider:
// - all registered Typefaces are collected in registeredTypefaces
// - then they are used to find a fallback Typeface in `fallbackForChar`
class TypefaceFontProviderWithFallback : public TypefaceFontProvider {
public:
size_t registerTypeface(sk_sp<SkTypeface> typeface);
size_t registerTypeface(sk_sp<SkTypeface> typeface, const SkString& alias);

sk_sp<SkTypeface> fallbackForChar(SkUnichar character) const;

private:
std::vector<sk_sp<SkTypeface>> registeredTypefaces;
};

// FontMgrWithFallbackWrapper implementation in most methods simply delegates to wrappedFntMgr.
// The only modified implementation is onMatchFamilyStyleCharacter, which attempts to find
// any Typeface (fallback) for a given character, if all other default fallbacks didn't find anything.
// To achieve this it relies on fallbackFontProvider set via `setFallbackFontProvider`.
class FontMgrWithFallbackWrapper : public SkFontMgr {
public:
explicit FontMgrWithFallbackWrapper(sk_sp<TypefaceFontProviderWithFallback>);
int onCountFamilies() const override;
void onGetFamilyName(int index, SkString* familyName) const override;
sk_sp<SkFontStyleSet> onMatchFamily(const char familyName[]) const override;
sk_sp<SkFontStyleSet> onCreateStyleSet(int) const override;
sk_sp<SkTypeface> onMatchFamilyStyle(const char[], const SkFontStyle&) const override;
sk_sp<SkTypeface> onMatchFamilyStyleCharacter(const char[], const SkFontStyle&,
const char*[], int,
SkUnichar) const override;
sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int) const override;
sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>, int) const override;
sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>,
const SkFontArguments&) const override;
sk_sp<SkTypeface> onMakeFromFile(const char[], int) const override;

sk_sp<SkTypeface> onLegacyMakeTypeface(const char[], SkFontStyle) const override;

void setFallbackFontProvider(sk_sp<TypefaceFontProviderWithFallback>);

private:
sk_sp<SkFontMgr> wrappedFntMgr;
sk_sp<TypefaceFontProviderWithFallback> fallbackFontProvider;
};


#endif
1 change: 1 addition & 0 deletions skiko/src/commonMain/kotlin/org/jetbrains/skia/FontMgr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.skia

import org.jetbrains.skia.impl.*
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.paragraph.TypefaceFontProvider

open class FontMgr : RefCnt {
companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.jetbrains.skia

import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.paragraph.TypefaceFontProviderWithFallback

/**
* Consider registering this FontMgr as a default one
* to let all other possibly registered Font managers to look for their fallbacks first:
* `FontCollection.setDefaultFontManager(...)`
*
* The fallbacks provided by this class will be used a last resort.
*/
class FontMgrWithFallback(
fallbackProvider: TypefaceFontProviderWithFallback
) : FontMgr(_nDefaultWithFallbackFontProvider(fallbackProvider._ptr))

@ExternalSymbolName("org_jetbrains_skia_FontMgrWithFallback__1nDefaultWithFallbackFontProvider")
@ModuleImport("./skiko.mjs", "org_jetbrains_skia_FontMgrWithFallback__1nDefaultWithFallbackFontProvider")
private external fun _nDefaultWithFallbackFontProvider(fallbackPtr: NativePointer): NativePointer
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.getPtr
import org.jetbrains.skia.impl.interopScope

class TypefaceFontProvider : FontMgr(TypefaceFontProvider_nMake()) {
open class TypefaceFontProvider internal constructor(
ptr: NativePointer
) : FontMgr(ptr) {

constructor(): this(TypefaceFontProvider_nMake())

companion object {
init {
staticLoad()
}
}

fun registerTypeface(typeface: Typeface?, alias: String? = null): TypefaceFontProvider {
open fun registerTypeface(typeface: Typeface?, alias: String? = null): TypefaceFontProvider {
return try {
Stats.onNativeCall()
interopScope {
Expand Down Expand Up @@ -48,4 +53,4 @@ private external fun TypefaceFontProvider_nMake(): NativePointer

@ExternalSymbolName("org_jetbrains_skia_paragraph_TypefaceFontProvider__1nRegisterTypeface")
@ModuleImport("./skiko.mjs", "org_jetbrains_skia_paragraph_TypefaceFontProvider__1nRegisterTypeface")
private external fun _nRegisterTypeface(ptr: NativePointer, typefacePtr: NativePointer, alias: InteropPointer)
private external fun _nRegisterTypeface(ptr: NativePointer, typefacePtr: NativePointer, alias: InteropPointer): Int
Loading

0 comments on commit 45c1a7d

Please sign in to comment.