first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

19
node_modules/expo-font/android/build.gradle generated vendored Normal file
View File

@@ -0,0 +1,19 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'host.exp.exponent'
version = '55.0.4'
android {
namespace "expo.modules.font"
defaultConfig {
versionCode 29
versionName "55.0.4"
}
}
dependencies {
implementation 'com.facebook.react:react-android'
}

View File

@@ -0,0 +1,3 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,77 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.font
import android.content.Context
import android.graphics.Typeface
import android.net.Uri
import com.facebook.react.common.assets.ReactFontManager
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.io.File
private const val ASSET_SCHEME = "asset://"
private class FileNotFoundException(uri: String) :
CodedException("File '$uri' doesn't exist")
open class FontLoaderModule : Module() {
private val context: Context
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
override fun definition() = ModuleDefinition {
// could be a Set, but to be able to pass to JS we keep it as an array
var loadedFonts: List<String> = queryCustomNativeFonts()
Name("ExpoFontLoader")
Function("getLoadedFonts") {
return@Function loadedFonts
}
AsyncFunction("loadAsync") { fontFamilyName: String, localUri: String ->
val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
// TODO(nikki): make sure path is in experience's scope
val typeface: Typeface = if (localUri.startsWith(ASSET_SCHEME)) {
Typeface.createFromAsset(
context.assets, // Also remove the leading slash.
localUri.substring(ASSET_SCHEME.length + 1)
)
} else {
val file = Uri.parse(localUri).path?.let { File(it) }
?: throw FileNotFoundException(localUri)
if (file.length() == 0L) {
throw CodedException(
"Font file for $fontFamilyName is empty. Make sure the local file path is correctly populated."
)
}
Typeface.createFromFile(file)
}
ReactFontManager.getInstance().setTypeface(fontFamilyName, Typeface.NORMAL, typeface)
loadedFonts = loadedFonts.toMutableSet().apply { add(fontFamilyName) }.toList()
}
}
/**
* Queries custom native font names from the assets.
* Alignment with React Native's implementation at:
* https://github.com/facebook/react-native/blob/363ee484b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/assets/ReactFontManager.java#L146-L161
*/
private fun queryCustomNativeFonts(): List<String> {
val assetManager = context.assets
val fontFileRegex = Regex("^(.+?)(_bold|_italic|_bold_italic)?\\.(ttf|otf)$")
return assetManager.list("fonts/")
?.mapNotNull { fileName ->
fontFileRegex.find(fileName)?.groupValues?.get(1)
}
?.filter { it.isNotBlank() }
.orEmpty()
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.font
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import androidx.core.net.toUri
import com.facebook.react.common.assets.ReactFontManager
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
import kotlin.math.ceil
private class SaveImageException(uri: String, cause: Throwable? = null) :
CodedException("Could not save image to '$uri'", cause)
open class FontUtilsModule : Module() {
private val context: Context
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
override fun definition() = ModuleDefinition {
Name("ExpoFontUtils")
AsyncFunction("renderToImageAsync") { glyphs: String, options: RenderToImageOptions, promise: Promise ->
val typeface = ReactFontManager.getInstance().getTypeface(options.fontFamily, Typeface.NORMAL, context.assets)
val scalingFactor = context.resources.displayMetrics.density
val scaledSize = options.size * scalingFactor
val lineHeight = options.lineHeight?.let { it * scalingFactor }
val paint = Paint().apply {
this.typeface = typeface
color = options.color
textSize = scaledSize
isAntiAlias = true
}
val fontMetrics = paint.fontMetrics
val width = ceil(paint.measureText(glyphs)).toInt()
// Calculate height based on font metrics to ensure enough space
// This gives the maximum height the font might occupy. Could be more than strictly needed but aligns with iOS.
val height = lineHeight?.toInt() ?: ceil(fontMetrics.descent - fontMetrics.ascent).toInt()
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val yBaseline = lineHeight?.let {
// When lineHeight is specified, center the text vertically within that height
(it - (fontMetrics.descent - fontMetrics.ascent)) / 2f - fontMetrics.ascent
// The `drawText` method's y-parameter is the baseline of the text.
// To draw the text starting from the very top of the bitmap,
// the baseline should be at -fontMetrics.ascent.
// For most characters, text may appear vertically centered, but try with characters like Å or Ç
} ?: -fontMetrics.ascent
canvas.drawText(glyphs, 0f, yBaseline, paint)
val output = File(context.cacheDir, "${UUID.randomUUID()}.png")
try {
FileOutputStream(output).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
promise.resolve(
mapOf(
"uri" to output.toUri().toString(),
"width" to bitmap.width / scalingFactor,
"height" to bitmap.height / scalingFactor,
"scale" to scalingFactor
)
)
} catch (e: IOException) {
promise.reject(SaveImageException(output.absolutePath, e))
}
}
}
}

View File

@@ -0,0 +1,19 @@
package expo.modules.font
import android.graphics.Color
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class RenderToImageOptions(
@Field
val fontFamily: String = "",
@Field
val size: Float = 24f,
@Field
val lineHeight: Float? = null,
@Field
val color: Int = Color.BLACK
) : Record