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.
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.