Skip to main content Skip to docs navigation

Imaging Text

KorIM support creating, rendering, and measuring TTF Vector fonts and Bitmap fonts. In addition it support generating bitmap fonts on the fly, and storing them.

Document not reviewed yet, might be outdated. Please, let us know if you find something invalid here.
On this page

Font

Font is the common interface for all the available fonts independently to their type. KorIM provides several implementations: BitmapFont and VectorFont. VectorFont is also divided in TtfFont and SystemFont.

All the fonts have several available methods, for information, metrics and rendering:

interface Font {
	// Name
    val name: String

    // Metrics
    fun getFontMetrics(size: Double, metrics: FontMetrics = FontMetrics()): FontMetrics
    fun getGlyphMetrics(size: Double, codePoint: Int, metrics: GlyphMetrics = GlyphMetrics()): GlyphMetrics
    fun getKerning(size: Double, leftCodePoint: Int, rightCodePoint: Int): Double

    // Rendering
    fun renderGlyph(ctx: Context2d, size: Double, codePoint: Int, x: Double, y: Double, fill: Boolean, metrics: GlyphMetrics)
}

BitmapFont

Bitmap Fonts are fonts that include all their glyphs in one or several atlases. They have several:

  • Ideal for fast changing texts
  • Supports color glyphs, gradients, shadows, or whatever effect you want put in each glyph
  • They tend to take more space to download
  • Load pretty fast
  • Do not scale well, your rendered glyphs should have a reasonable size

class BitmapFont : Font {
    // ...
    
    // Getting a glyph from a codePoint/character
    operator fun get(codePoint: Int): Glyph
    operator fun get(char: Char): Glyph
    
    class Glyph {
        val fontSize: Double
        val id: Int
        val texture: BitmapSlice<Bitmap>
        val xoffset: Int
        val yoffset: Int
        val xadvance: In
        val bmp: Bitmap32
    }
}

Reading a BitmapFont

With any VfsFile reference to the bitmap font descriptor, you can load it with readBitmapFont and readFont:

val font: BitmapFont = resourcesVfs["font.fnt"].readBitmapFont()
val font: Font = resourcesVfs["font.fnt"].readFont() // Supports TTF and Bitmap Fonts

It supports and automatically detects and loads text, JSON and XML formats. Typical formats generated by programs to generate bitmap fonts should work here.

Creating a BitmapFont from a TtfFont on the fly

You can create a BitmapFont from another Font (including vector, system and bitmap fonts) with the Font.toBitmapFont method, for a specific size, including a set of characters and specifying and paint and optionally some effects.

val baseFont: Font = // any font including TTF or Bitmap here
val font: BitmapFont = baseFont.toBitmapFont(
    fontSize = 32.0,
    chars = CharacterSet.LATIN_ALL,
    paint = Colors.WHITE,
    mipmaps = true,
    effect = BitmapEffect(
        dropShadowX = 2, dropShadowY = 1, dropShadowRadius = 2,
        borderSize = 2, borderColor = Colors.RED,
    ) 
) 

Writing a BitmapFont to a file

In some cases, for example after creating a BitmapFont from another font, you might want to store that font, into a VfsFile. To do so, you can use the BitmapFont.writeToFile method.

val mem = MemoryVfs()
val font: BitmapFont = BitmapFont(DefaultTtfFont, fontSize = 16.0)
font.writeToFile(mem["font.fnt"])
localCurrentDirVfs["myfont.zip"].writeBytes(mem.createZipFromTree())

In this example, the bitmap font is stored into an in-memory virtual file system along the atlas, then it generates a zip file with a font.fnt and font.png files, and stores it in a file called myfont.zip in the current directory.

TtfFont

TTF Fonts are the most popular vector font available. Glyphs are described as vectors, and normally do not have color information except for some specific fonts. Latest versions experimentally support some ttf fonts with colors, and embedded bitmaps like emojis.

  • Good for static texts
  • Can scale to any size
  • Usually smaller
  • Each glyph will require parsing

Reading a TtfFont

With any VfsFile reference to the TTF file, you can load it with readTtfFont and readFont:

val font: BitmapFont = resourcesVfs["font.ttf"].readTtfFont()
val font: Font = resourcesVfs["font.ttf"].readFont() // Supports TTF and Bitmap Fonts

It supports TTF, but doesn’t support Open Type Fonts (OTF) yet, it doesn’t support WOFF (zlib), WOFF2 (brotli) formats (compressed TTF fonts) either.

There is a default TTF font embedded in the library. You can access that font with the global DefaultTtfFont.

VectorFont

A VectorFont is any font that supports vector scalable rendering. Here we have TtfFonts and SystemFonts (that end being a TTF font too). Adding support for new vector fonts, should be included here.

In addition to the methods provided by Font, the vector font, allows to get a GlyphPath with the vector commands of each glyph.

interface VectorFont : Font {
    fun getGlyphPath(size: Double, codePoint: Int, path: GlyphPath = GlyphPath()): GlyphPath?
}

GlyphPath

class GlyphPath : Drawable {
    var path: GraphicsPath
    var colorPaths: List<Drawable>?
    var bitmap: Bitmap?
    val bitmapOffset: Point
    val bitmapScale: Point
    val transform: Matrix
    var scale: Double
    
    // Draw to a Context2d
    fun draw(c: Context2d)
}

SystemFont

A SystemFont is a font that is provided by the underlying Operating System. Current implementation creates a catalog and just searches for .ttf files in well-known directories. A SystemFont is referenced by a name

Constructing a SystemFont

SystemFonts are constructed by their name.

val font = SystemFont("Arial")

Listing font names

To determine the list of fonts available on the current system, you can use two functions:

val fontNames: List<String> = SystemFont.listFontNames()
val fontNamesToFile: Map<String, VfsFile> = SystemFont.listFontNamesWithFiles()

Getting default font and emoji font from the system

val defaultFont: SystemFont = SystemFont.getDefaultFont()
val defaultFont: SystemFont = SystemFont.getEmojiFont()

Catalog cache file

When using SystemFonts, KorIM explores all the .TTF fonts in the system, and generates a cache mapping names to their corresponding TTF file. It is optimized, and it is usually a fast process, but might take some noticeable time. After generating it the first time, it will create a font cache file:

~/.korimFontCache

NativeSystemFontProvider

There is a global instance called nativeSystemFontProvider, that is specific per system, that allows to interact with system fonts.

val nativeSystemFontProvider: NativeSystemFontProvider

class NativeSystemFontProvider {
    fun getDefaultFontName(): String
    fun getEmojiFontName(): String
    fun listFontNames(): List<String>
    fun getTtfFromSystemFont(systemFont: SystemFont): TtfFont
    fun listFontNamesWithFiles(): Map<String, VfsFile>
    fun getSystemFontGlyph(systemFont: SystemFont, size: Double, codePoint: Int, path: GlyphPath = GlyphPath()): GlyphPath?
    fun getSystemFontMetrics(systemFont: SystemFont, size: Double, metrics: FontMetrics)
    fun getSystemFontGlyphMetrics(systemFont: SystemFont, size: Double, codePoint: Int, metrics: GlyphMetrics)
    fun getSystemFontKerning(systemFont: SystemFont, size: Double, leftCodePoint: Int, rightCodePoint: Int) : Double
}

DefaultTtfFont

There is a default TTF font embedded in the library. You can access that font with the global DefaultTtfFont.

FontRegistry

When dealing with our custom fonts, we might want to use a catalog/registry of fonts, so we can get them by name.

interface FontRegistry {
    operator fun get(name: String?): Font 
    companion object {
        operator fun invoke(): DefaultFontRegistry
    }
}

val SystemFontRegistry: DefaultFontRegistry

class DefaultFontRegistry : FontRegistry {
    fun register(font: Font, name: String = font.name): Font
    fun unregister(name: String): Font?
    inline fun <T> registerTemporarily(font: Font, name: String = font.name, block: () -> T): T
}

fun <T : Font> T.register(registry: DefaultFontRegistry = SystemFontRegistry, name: String = this.name): T
inline fun <T> Font.registerTemporarily(registry: DefaultFontRegistry = SystemFontRegistry, name: String = this.name, block: () -> T): T

Metrics

When dealing with fonts, we often require getting metrics for measuring different aspects of the font, text, lines, individual glyphs or kernings. KorIM provides several classes with metrics about different aspects of each font.

FontMetrics

For global font information metrics, we have FontMetrics.

class FontMetrics {
    /** size of the font metric */
    var size: Double
    /** maximum top for any character like É  */
    var top: Double
    /** ascent part of E */
    var ascent: Double
    /** base of 'E' */
    var baseline: Double // Should be 0.0
    /** lower part of 'j' */
    var descent: Double
    /** descent + linegap */
    var bottom: Double
    /** extra height */
    var leading: Double
    /** maximum number of width */
    var maxWidth: Double

    /* 'E' height */
    val emHeight: Double
    /* 'É' + 'j' + linegap */
    val lineHeight: Double
}

GlyphMetrics

For specific glyphs we can retrieve several properties.

class GlyphMetrics {
    var size: Double
    var existing: Boolean
    var codePoint: Int
    val bounds: Rectangle
    var xadvance: Double

    val right: Double
    val bottom: Double
    val left: Double
    val top: Double
    val width: Double
    val height: Double
}

TextMetrics

For full text placements using a TextRenderer, we can measure different aspects of the placement.

fun <T> Font.measureTextGlyphs(
    size: Double,
    text: T,
    renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>,
): TextMetricsResult

class TextMetricsResult {
    val fmetrics: FontMetrics,
    val metrics: TextMetrics,
    val glyphs: List<PlacedGlyphMetrics>
    val fmetrics: FontMetrics
    val metrics: TextMetrics
    val glyphs: List<PlacedGlyphMetrics>
)

data class PlacedGlyphMetrics(val codePoint: Int, val x: Double, val y: Double, val metrics: GlyphMetrics, val transform: Matrix, val index: Int)

class TextMetrics {
    val bounds: Rectangle
    val firstLineBounds: Rectangle
    val fontMetrics: FontMetrics
    var nlines: Int

    val left: Double
    val top: Double

    val right: Double
    val bottom: Double

    val width: Double
    val height: Double

    val drawLeft: Double
    val drawTop: Double

    val ascent: Double
    val descent: Double
    val lineHeight: Double
    val allLineHeight: Double
}

CharacterSet

CharacterSet is used for specifying the glyphs to be included when creating new BitmapFont.

class CharacterSet(val codePoints: IntArray) {
    constructor(chars: String)
    
    operator fun plus(other: CharacterSet): CharacterSet

    companion object {
        val SPACE = CharacterSet(" ")
        val UPPERCASE = CharacterSet(('A'..'Z').joinToString(""))
        val LOWERCASE = CharacterSet(('a'..'z').joinToString(""))
        val NUMBERS = CharacterSet(('0'..'9').joinToString(""))
        val PUNCTUATION = CharacterSet("!\"#\$%&'()*+,-./:;<=>?@[\\]^_`{|}")
        val LATIN_BASIC = CharacterSet("çÇ ñÑ åÅ æÆ ÿ ¢£¥Pª°¿¬½¼¡«»ßµø±÷°·.² áéíóúäëïöüàèìòùâêîôû ÁÉÍÓÚÄËÏÖÜÀÈÌÒÙÂÊÎÔÛ")
        val CYRILLIC = CharacterSet("АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ")
        val LATIN_ALL = SPACE + UPPERCASE + LOWERCASE + NUMBERS + PUNCTUATION + LATIN_BASIC
    }
}

Combining CharacterSet

You can combine several CharacterSet into a single one with the + operator to pass it to the toBitmapFont method when constructing a new BitmapFont.

val characterSet: CharacterSet = CharacterSet.UPPERCASE + CharacterSet.LOWERCASE + CharacterSet("+-") 

TextRenderer

val DefaultStringTextRenderer: TextRenderer<String>

interface TextRenderer<T> {
    val version: Int
    fun TextRendererActions.run(text: T, size: Double, defaultFont: Font): Unit
}

Default text renderers

inline fun <reified T> DefaultTextRenderer() = when (T::class) {
    String::class -> DefaultStringTextRenderer
    else -> error("No default DefaultTextRenderer for class ${T::class} only for String")
}

fun CreateStringTextRenderer(
    getVersion: () -> Int = { 0 },
    handler: TextRendererActions.(text: String, n: Int, c: Int, c1: Int, g: GlyphMetrics, advance: Double) -> Unit
): TextRenderer<String>

TextRendererActions

abstract class TextRendererActions {
    protected val glyphPath: GlyphPath
    protected val glyphMetrics: GlyphMetrics
    val fontMetrics: FontMetrics
    val lineHeight: Double
    
    lateinit var font: Font; private set
    
    var fontSize = 0.0; private set

    fun setFont(font: Font, size: Double)

    var x = 0.0
    var y = 0.0

    fun getGlyphMetrics(codePoint: Int): GlyphMetrics

    val transform: Matrix
    var paint: Paint?
    var tint: RGBA

    open fun reset()
    open fun getKerning(leftCodePoint: Int, rightCodePoint: Int): Double
    open fun advance(x: Double)
    open fun newLine(y: Double)

    abstract fun put(codePoint: Int): GlyphMetrics
}

Creating a custom TextRenderer

    open fun reset()
    open fun getKerning(leftCodePoint: Int, rightCodePoint: Int): Double
    open fun advance(x: Double)
    open fun newLine(y: Double)

    abstract fun put(codePoint: Int): GlyphMetrics

TextAlignment

When drawing texts you might want to provide a text alignment, to render the text from a specific point determined by the text metrics.


class TextAlignment(
    val horizontal: HorizontalAlign,
    val vertical: VerticalAlign,
) {
    val justified: Boolean get() = horizontal == HorizontalAlign.JUSTIFY
    val anchor: Anchor = Anchor(horizontal.ratioFake, vertical.ratioFake)

    fun withHorizontal(horizontal: HorizontalAlign): TextAlignment
    fun withVertical(vertical: VerticalAlign): TextAlignment

    companion object {
        val TOP_LEFT: TextAlignment
        val TOP_CENTER: TextAlignment
        val TOP_RIGHT: TextAlignment
        val TOP_JUSTIFIED: TextAlignment

        val BASELINE_LEFT: TextAlignment
        val BASELINE_CENTER: TextAlignment
        val BASELINE_RIGHT: TextAlignment
        val BASELINE_JUSTIFIED: TextAlignment

        val LEFT: TextAlignment
        val CENTER: TextAlignment
        val RIGHT: TextAlignment
        val JUSTIFIED: TextAlignment

        val MIDDLE_LEFT: TextAlignment
        val MIDDLE_CENTER: TextAlignment
        val MIDDLE_RIGHT: TextAlignment
        val MIDDLE_JUSTIFIED: TextAlignment

        val BOTTOM_LEFT: TextAlignment
        val BOTTOM_CENTER: TextAlignment
        val BOTTOM_RIGHT: TextAlignment
        val BOTTOM_JUSTIFIED: TextAlignment

        fun fromAlign(horizontal: HorizontalAlign, vertical: VerticalAlign): TextAlignment
    }
}

inline class VerticalAlign(val ratio: Double) {
    val ratioFake: Double get() = if (this == BASELINE) 1.0 else ratio
    fun getOffsetY(height: Double, baseline: Double): Double

    companion object {
        val TOP = VerticalAlign(0.0)
        val MIDDLE = VerticalAlign(0.5)
        val BOTTOM = VerticalAlign(1.0)
        val BASELINE = VerticalAlign(Double.POSITIVE_INFINITY) // Special

        operator fun invoke(str: String): VerticalAlign
    }
}

inline class HorizontalAlign(val ratio: Double) {
    val ratioFake: Double get() = if (this == JUSTIFY) 0.0 else ratio
    fun getOffsetX(width: Double): Double

    companion object {
        val JUSTIFY:  = HorizontalAlign(-0.00001)
        val LEFT = HorizontalAlign(0.0)
        val CENTER = HorizontalAlign(0.5)
        val RIGHT = HorizontalAlign(1.0)
        operator fun invoke(str: String): HorizontalAlign
    }
}

Drawing

Once we have already our Font instance created, we can use it to render stuff in bitmaps, in VectorBuilders or in Context2d.

Drawing text into a VectorBuilder with a TextRenderer

fun <T> VectorBuilder.text(
    text: T, font: VectorFont, textSize: Double = 16.0,
    x: Double = 0.0, y: Double = 0.0,
    renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>,
)

Rendering text and individual glyphs to a Bitmap

fun <T> Font.renderTextToBitmap(
    size: Double,
    text: T,
    paint: Paint = DefaultPaint,
    background: Paint = NonePaint,
    fill: Boolean = true,
    border: Int = 0,
    renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>,
    returnGlyphs: Boolean = true,
    nativeRendering: Boolean = true,
    drawBorder: Boolean = false
): TextToBitmapResult

fun Font.renderGlyphToBitmap(
	size: Double, codePoint: Int, paint: Paint = DefaultPaint, fill: Boolean = true,
	effect: BitmapEffect? = null,
	border: Int = 1, nativeRendering: Boolean = true
): TextToBitmapResult 

Rendering in Context2d

// For drawing a text
fun <T> Context2d.drawText(text: T, x: Double = 0.0, y: Double = 0.0, fill: Boolean = true, paint: Paint? = null, font: Font? = this.font, size: Double = this.fontSize, renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>)

// Simple shortcuts
fun Context2d.fillText(text: String, x: Double, y: Double)
fun Context2d.strokeText(text: String, x: Double, y: Double)

// Context2d is also a VectorBuilder

fun <T> VectorBuilder.text(
    text: T, font: VectorFont, textSize: Double = 16.0,
    x: Double = 0.0, y: Double = 0.0,
    renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>,
)

// Using Font as receiver
fun <T> Font.drawText(
    ctx: Context2d?, size: Double,
    text: T, paint: Paint?,
    x: Double = 0.0, y: Double = 0.0,
    fill: Boolean = true,
    renderer: TextRenderer<T> = DefaultStringTextRenderer as TextRenderer<T>,
    placed: ((codePoint: Int, x: Double, y: Double, size: Double, metrics: GlyphMetrics, transform: Matrix) -> Unit)? = null
)

// Measuring
fun Context2d.getTextBounds(text: String, out: TextMetrics = TextMetrics()): TextMetrics

Korge Views

KorGE provides integration with KorIM to have a view to render texts with Bitmap and Vector fonts. There is a Text and TextOld instances for creating views rendering texts. You can check the korge-samples repository, and the KorGE documentation for this views.

Was this article useful?