
TrueType font rendering using FreeType/HarfBuzz: surface and renderer text engines, GPU SDF pipeline, glyph metrics/kerning, IOStream font loading, live editable text with substring inspection.
Kotlin Multiplatform bindings for SDL_ttf 3 (TrueType font rendering with FreeType/HarfBuzz), built on top of sdl-kmp. The public API lives in the cn.enaium.sdl.ttf package and works directly with the sdl-kmp types (SDLRenderer, SDLSurface, SDLIOStream, SDLColor, ...).
Two implementations, mirroring sdl-kmp:
SDL_ttf submodule) are compiled by CMake (jni/) into a JNI shared library (libsdl_ttf_jni), shipped as per-OS/arch sdl-ttf-kmp-jni-jvm-* artifacts — the same self-contained approach as sdl-kmp's libsdl_jni. TtfNativeLoader extracts the matching binary at runtime. The process contains a second SDL3 copy; SDL_ttf errors are read through the TTF-side SDL_GetError (SDLTTF.error()), and SDL objects from the sdl-kmp library are operated on through SDL3's function-pointer interfaces, so the copies do not interfere.cn.enaium.sdl types.| Platform | Targets | Implementation |
|---|---|---|
| JVM |
jvm (Linux/macOS/Windows) |
JNI shared library (libsdl_ttf_jni), SDL3 + SDL_ttf compiled from source |
| macOS |
macosArm64, macosX64
|
cinterop + embedded static SDL_ttf |
| Linux |
linuxX64, linuxArm64
|
cinterop + embedded static SDL_ttf |
| Windows | mingwX64 |
cinterop + embedded static SDL_ttf |
| iOS |
iosArm64, iosX64, iosSimulatorArm64
|
cinterop + embedded static SDL_ttf |
| tvOS |
tvosArm64, tvosSimulatorArm64
|
cinterop + embedded static SDL_ttf |
| Android |
androidNativeArm64, androidNativeArm32, androidNativeX64, androidNativeX86
|
cinterop + embedded static SDL_ttf (built with the NDK) |
The published version requires sdl-kmp 1.0.7 (it is an api dependency, pulled in automatically).
build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("cn.enaium.sdl:sdl-ttf-kmp:1.0.0")
}
}
}import cn.enaium.sdl.*
import cn.enaium.sdl.ttf.*
fun main() {
SDL.setMainReady()
if (!SDL.init(SDLInitFlags.VIDEO or SDLInitFlags.EVENTS)) {
error("SDL_Init failed: ${SDL.error()}")
}
if (!SDLTTF.init()) {
error("TTF_Init failed: ${SDLTTF.error()}")
}
SDL.createWindow("sdl-ttf-kmp", 800, 600).use { window ->
SDL.createRenderer(window).use { renderer ->
val font = SDLTTF.openFont("/path/to/font.ttf", 32f)
// 1) Render text to a surface, upload it into a texture and draw it.
val textSurface = SDLTTF.renderTextBlended(font, "Hello, SDL_ttf!", SDLColor(240, 240, 240))
?: error("render failed: ${SDLTTF.error()}")
val texture = renderer.createTexture(
format = textSurface.format,
access = SDLTextureAccess.STATIC,
width = textSurface.width,
height = textSurface.height,
)
texture.update(null, textSurface.pixels, textSurface.pitch)
textSurface.close()
// 2) Or use the renderer text engine: SDL_ttf draws directly on
// the sdl-kmp renderer, and the text re-lays out automatically.
val engine = SDLTTF.createRendererTextEngine(renderer)
val text = SDLTTF.createText(engine, font, "Live text engine")
text.color = SDLColor(255, 200, 60)
text.wrapWidth = 300
renderer.drawColor = SDLColor(18, 18, 24)
renderer.clear()
renderer.renderTexture(texture, dst = SDLFRect(40f, 40f, texture.size.x, texture.size.y))
text.draw(40f, 120f)
renderer.present()
text.close()
engine.close()
font.close()
}
}
SDLTTF.quit()
SDL.quit()
}SDLTTF.openFont, SDLTTF.openFontIO (from a cn.enaium.sdl.SDLIOStream), SDLTTF.copyFont; SDLTTFFont exposes size/style/outline/hinting/SDF/kerning/direction/char-spacing, metrics (height/ascent/descent/lineSkip/weight), getStringSize, getStringSizeWrapped, measureString, glyph metrics/kerning and fallback fonts.renderTextSolid/Shaded/Blended/LCD (+ _Wrapped) and renderGlyph* return SDLSurfaces; wrap them into a texture with renderer.createTexture + SDLTexture.update (see the note below).SDLTTF.createRendererTextEngine(renderer) draws SDLTTFText objects directly on an sdl-kmp SDLRenderer via SDLTTFText.draw; SDLTTF.createSurfaceTextEngine draws on software surfaces via SDLTTFText.drawOnSurface. Text objects support color, position, wrap width, wrap-whitespace visibility, string editing and substring lookup (subStringAt/subStringForLine/subStringForPoint).SDLTTF.error().-XstartOnFirstThread (the example jvmRun task already sets it). With a HiDPI window, rasterize text at sizeInPixels / size and draw at logical size for crisp output (the example does this; see TTFTextDemo.dpiScale).sdl-ttf-kmp-jni-jvm-{os}-{arch} artifact is a transitive runtime dependency; TtfNativeLoader extracts libsdl_ttf_jni and System.load()s it. libsdl_ttf_jni bundles its own SDL3, so no java.library.path setup is needed.SDLTexture.createTextureFromSurface downcasts the surface to sdl-kmp's internal implementation, so SDL_ttf-rendered surfaces must be uploaded with createTexture + SDLTexture.update instead.androidNative* target requires an installed Android NDK (found under $ANDROID_HOME/ndk); the SDL_ttf static library is cross-compiled with its CMake toolchain.SDL_VIDEO_DRIVER=dummy (hint or environment variable) to run without a display; SDL_ttf itself does not need video.examples/ttf_renderer — a renderer demo on top of sdl-kmp's 2D renderer: surface-rendered text (blended/shaded/LCD/supersampled) uploaded into textures, the renderer text engine (animated, wrapped, resizable text), the surface text engine (drawing on a software surface), fonts opened from files and SDL_IOStreams, metrics/kerning/substring inspection, and CJK font resolution. Runs on JVM, macOS, Linux and Windows (MinGW):# headless (CI / servers)
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:jvmRun --args="fonts/DejaVuSans.ttf"
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:runDebugExecutableMacosArm64
# with a window
./gradlew :examples:ttf_renderer:jvmRunControls: ESC quit, UP/DOWN font size, SPACE toggle wrapping, W wrap whitespace, C cycle color, click to inspect substrings.
examples/ttf_gpu — text laid out with the SDL_ttf GPU text engine
(SDLTTF.createGPUTextEngine + SDLTTFText.getGPUDrawData) and rendered
with the SDL3 GPU API from commonMain: MSL shaders on Metal, precompiled
SPIR-V elsewhere; per frame the pixel-space vertices are transformed to
NDC on the CPU, uploaded into vertex/index buffers and drawn with
drawIndexedPrimitives. A dedicated SDF pipeline (smoothstep on the
signed distance) renders SDF fonts sharp at any scale. Requires a GPU
(not available with the dummy driver).Requirements: JDK 21, CMake, a C/C++ compiler; Xcode for Apple targets, the x86_64-w64-mingw32-gcc toolchain for MinGW cross-compiles (Linux host), the Android NDK for androidNative*.
git clone --recurse-submodules git@github.com:Enaium/sdl-ttf-kmp.git
cd sdl-ttf-kmp
# compile + test the JVM target
./gradlew :sdl-ttf-kmp:jvmTest
# run the example headless
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:jvmRun
# publish everything buildable on this host to Maven Local
./gradlew :sdl-ttf-kmp:publishToMavenLocal :ttf-jni-jvm-darwin-aarch64:publishToMavenLocalBoth workflows are manually triggered (Actions tab):
test.yml — local Maven publish + test: publishes every artifact the runner can build to Maven Local (no signing, no secrets), runs the JVM/native tests and the example headless. Use this to verify a change before publishing.publish.yml — formal Maven Central release: publishes the metadata + JVM module, all target klibs and the JNI artifacts to Maven Central, signed with PGP. The version is taken from the workflow input (-PsdlTtfVersion, default 1.0.0). Requires the repository secrets MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_KEY_ID and SIGNING_PASSWORD.MIT. The bundled SDL3 submodule is licensed under the zlib license.
Kotlin Multiplatform bindings for SDL_ttf 3 (TrueType font rendering with FreeType/HarfBuzz), built on top of sdl-kmp. The public API lives in the cn.enaium.sdl.ttf package and works directly with the sdl-kmp types (SDLRenderer, SDLSurface, SDLIOStream, SDLColor, ...).
Two implementations, mirroring sdl-kmp:
SDL_ttf submodule) are compiled by CMake (jni/) into a JNI shared library (libsdl_ttf_jni), shipped as per-OS/arch sdl-ttf-kmp-jni-jvm-* artifacts — the same self-contained approach as sdl-kmp's libsdl_jni. TtfNativeLoader extracts the matching binary at runtime. The process contains a second SDL3 copy; SDL_ttf errors are read through the TTF-side SDL_GetError (SDLTTF.error()), and SDL objects from the sdl-kmp library are operated on through SDL3's function-pointer interfaces, so the copies do not interfere.cn.enaium.sdl types.| Platform | Targets | Implementation |
|---|---|---|
| JVM |
jvm (Linux/macOS/Windows) |
JNI shared library (libsdl_ttf_jni), SDL3 + SDL_ttf compiled from source |
| macOS |
macosArm64, macosX64
|
cinterop + embedded static SDL_ttf |
| Linux |
linuxX64, linuxArm64
|
cinterop + embedded static SDL_ttf |
| Windows | mingwX64 |
cinterop + embedded static SDL_ttf |
| iOS |
iosArm64, iosX64, iosSimulatorArm64
|
cinterop + embedded static SDL_ttf |
| tvOS |
tvosArm64, tvosSimulatorArm64
|
cinterop + embedded static SDL_ttf |
| Android |
androidNativeArm64, androidNativeArm32, androidNativeX64, androidNativeX86
|
cinterop + embedded static SDL_ttf (built with the NDK) |
The published version requires sdl-kmp 1.0.7 (it is an api dependency, pulled in automatically).
build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("cn.enaium.sdl:sdl-ttf-kmp:1.0.0")
}
}
}import cn.enaium.sdl.*
import cn.enaium.sdl.ttf.*
fun main() {
SDL.setMainReady()
if (!SDL.init(SDLInitFlags.VIDEO or SDLInitFlags.EVENTS)) {
error("SDL_Init failed: ${SDL.error()}")
}
if (!SDLTTF.init()) {
error("TTF_Init failed: ${SDLTTF.error()}")
}
SDL.createWindow("sdl-ttf-kmp", 800, 600).use { window ->
SDL.createRenderer(window).use { renderer ->
val font = SDLTTF.openFont("/path/to/font.ttf", 32f)
// 1) Render text to a surface, upload it into a texture and draw it.
val textSurface = SDLTTF.renderTextBlended(font, "Hello, SDL_ttf!", SDLColor(240, 240, 240))
?: error("render failed: ${SDLTTF.error()}")
val texture = renderer.createTexture(
format = textSurface.format,
access = SDLTextureAccess.STATIC,
width = textSurface.width,
height = textSurface.height,
)
texture.update(null, textSurface.pixels, textSurface.pitch)
textSurface.close()
// 2) Or use the renderer text engine: SDL_ttf draws directly on
// the sdl-kmp renderer, and the text re-lays out automatically.
val engine = SDLTTF.createRendererTextEngine(renderer)
val text = SDLTTF.createText(engine, font, "Live text engine")
text.color = SDLColor(255, 200, 60)
text.wrapWidth = 300
renderer.drawColor = SDLColor(18, 18, 24)
renderer.clear()
renderer.renderTexture(texture, dst = SDLFRect(40f, 40f, texture.size.x, texture.size.y))
text.draw(40f, 120f)
renderer.present()
text.close()
engine.close()
font.close()
}
}
SDLTTF.quit()
SDL.quit()
}SDLTTF.openFont, SDLTTF.openFontIO (from a cn.enaium.sdl.SDLIOStream), SDLTTF.copyFont; SDLTTFFont exposes size/style/outline/hinting/SDF/kerning/direction/char-spacing, metrics (height/ascent/descent/lineSkip/weight), getStringSize, getStringSizeWrapped, measureString, glyph metrics/kerning and fallback fonts.renderTextSolid/Shaded/Blended/LCD (+ _Wrapped) and renderGlyph* return SDLSurfaces; wrap them into a texture with renderer.createTexture + SDLTexture.update (see the note below).SDLTTF.createRendererTextEngine(renderer) draws SDLTTFText objects directly on an sdl-kmp SDLRenderer via SDLTTFText.draw; SDLTTF.createSurfaceTextEngine draws on software surfaces via SDLTTFText.drawOnSurface. Text objects support color, position, wrap width, wrap-whitespace visibility, string editing and substring lookup (subStringAt/subStringForLine/subStringForPoint).SDLTTF.error().-XstartOnFirstThread (the example jvmRun task already sets it). With a HiDPI window, rasterize text at sizeInPixels / size and draw at logical size for crisp output (the example does this; see TTFTextDemo.dpiScale).sdl-ttf-kmp-jni-jvm-{os}-{arch} artifact is a transitive runtime dependency; TtfNativeLoader extracts libsdl_ttf_jni and System.load()s it. libsdl_ttf_jni bundles its own SDL3, so no java.library.path setup is needed.SDLTexture.createTextureFromSurface downcasts the surface to sdl-kmp's internal implementation, so SDL_ttf-rendered surfaces must be uploaded with createTexture + SDLTexture.update instead.androidNative* target requires an installed Android NDK (found under $ANDROID_HOME/ndk); the SDL_ttf static library is cross-compiled with its CMake toolchain.SDL_VIDEO_DRIVER=dummy (hint or environment variable) to run without a display; SDL_ttf itself does not need video.examples/ttf_renderer — a renderer demo on top of sdl-kmp's 2D renderer: surface-rendered text (blended/shaded/LCD/supersampled) uploaded into textures, the renderer text engine (animated, wrapped, resizable text), the surface text engine (drawing on a software surface), fonts opened from files and SDL_IOStreams, metrics/kerning/substring inspection, and CJK font resolution. Runs on JVM, macOS, Linux and Windows (MinGW):# headless (CI / servers)
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:jvmRun --args="fonts/DejaVuSans.ttf"
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:runDebugExecutableMacosArm64
# with a window
./gradlew :examples:ttf_renderer:jvmRunControls: ESC quit, UP/DOWN font size, SPACE toggle wrapping, W wrap whitespace, C cycle color, click to inspect substrings.
examples/ttf_gpu — text laid out with the SDL_ttf GPU text engine
(SDLTTF.createGPUTextEngine + SDLTTFText.getGPUDrawData) and rendered
with the SDL3 GPU API from commonMain: MSL shaders on Metal, precompiled
SPIR-V elsewhere; per frame the pixel-space vertices are transformed to
NDC on the CPU, uploaded into vertex/index buffers and drawn with
drawIndexedPrimitives. A dedicated SDF pipeline (smoothstep on the
signed distance) renders SDF fonts sharp at any scale. Requires a GPU
(not available with the dummy driver).Requirements: JDK 21, CMake, a C/C++ compiler; Xcode for Apple targets, the x86_64-w64-mingw32-gcc toolchain for MinGW cross-compiles (Linux host), the Android NDK for androidNative*.
git clone --recurse-submodules git@github.com:Enaium/sdl-ttf-kmp.git
cd sdl-ttf-kmp
# compile + test the JVM target
./gradlew :sdl-ttf-kmp:jvmTest
# run the example headless
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:ttf_renderer:jvmRun
# publish everything buildable on this host to Maven Local
./gradlew :sdl-ttf-kmp:publishToMavenLocal :ttf-jni-jvm-darwin-aarch64:publishToMavenLocalBoth workflows are manually triggered (Actions tab):
test.yml — local Maven publish + test: publishes every artifact the runner can build to Maven Local (no signing, no secrets), runs the JVM/native tests and the example headless. Use this to verify a change before publishing.publish.yml — formal Maven Central release: publishes the metadata + JVM module, all target klibs and the JNI artifacts to Maven Central, signed with PGP. The version is taken from the workflow input (-PsdlTtfVersion, default 1.0.0). Requires the repository secrets MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_KEY_ID and SIGNING_PASSWORD.MIT. The bundled SDL3 submodule is licensed under the zlib license.