mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-07 11:51:50 +09:00
3148 lines
137 KiB
Kotlin
Executable File
3148 lines
137 KiB
Kotlin
Executable File
/*
|
||
* Terrarum Sans Bitmap
|
||
*
|
||
* Copyright (c) 2017-2026 see CONTRIBUTORS.txt
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to deal
|
||
* in the Software without restriction, including without limitation the rights
|
||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
* copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in all
|
||
* copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
* SOFTWARE.
|
||
*/
|
||
|
||
package net.torvald.terrarumsansbitmap.gdx
|
||
|
||
import com.badlogic.gdx.Gdx
|
||
import com.badlogic.gdx.graphics.Color
|
||
import com.badlogic.gdx.graphics.Pixmap
|
||
import com.badlogic.gdx.graphics.Texture
|
||
import com.badlogic.gdx.graphics.g2d.*
|
||
import com.badlogic.gdx.utils.GdxRuntimeException
|
||
import net.torvald.terrarumsansbitmap.DiacriticsAnchor
|
||
import net.torvald.terrarumsansbitmap.GlyphProps
|
||
import net.torvald.terrarumsansbitmap.MovableType
|
||
import net.torvald.terrarumsansbitmap.MovableType.Companion.GLUE_NEGATIVE_ONE
|
||
import net.torvald.terrarumsansbitmap.MovableType.Companion.GLUE_POSITIVE_SIXTEEN
|
||
import net.torvald.terrarumsansbitmap.TypesettingStrategy
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.FIXED_BLOCK_1
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.NBSP
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.OBJ
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.SHY
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.ZWSP
|
||
import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.glueCharToGlueSize
|
||
import java.io.BufferedOutputStream
|
||
import java.io.FileOutputStream
|
||
import java.util.*
|
||
import java.util.zip.CRC32
|
||
import java.util.zip.GZIPInputStream
|
||
import kotlin.math.floor
|
||
import kotlin.math.roundToInt
|
||
import kotlin.math.sign
|
||
|
||
class CodepointSequence: MutableList<CodePoint> {
|
||
private val data = ArrayList<CodePoint>()
|
||
|
||
constructor()
|
||
|
||
constructor(chars: Collection<CodePoint>) {
|
||
data.addAll(chars)
|
||
}
|
||
|
||
override val size; get() = data.size
|
||
val indices; get() = data.indices
|
||
val lastIndex; get() = data.lastIndex
|
||
|
||
fun forEach(action: (CodePoint) -> Unit) = data.forEach(action)
|
||
fun forEachIndexed(action: (Int, CodePoint) -> Unit) = data.forEachIndexed(action)
|
||
fun map(action: (CodePoint) -> Any?) = data.map(action)
|
||
fun mapInxeded(action: (Int, CodePoint) -> Any?) = data.mapIndexed(action)
|
||
fun first() = data.first()
|
||
fun firstOrNull() = data.firstOrNull()
|
||
fun first(predicate: (CodePoint) -> Boolean) = data.first(predicate)
|
||
fun firstOrNull(predicate: (CodePoint) -> Boolean) = data.firstOrNull(predicate)
|
||
fun last() = data.last()
|
||
fun lastOrNull() = data.lastOrNull()
|
||
fun last(predicate: (CodePoint) -> Boolean) = data.last(predicate)
|
||
fun lastOrNull(predicate: (CodePoint) -> Boolean) = data.lastOrNull(predicate)
|
||
fun filter(predicate: (CodePoint) -> Boolean) = data.filter(predicate)
|
||
override fun add(index: Int, char: CodePoint) = data.add(index, char)
|
||
override operator fun set(index: Int, char: CodePoint) = data.set(index, char)
|
||
override fun add(char: CodePoint) = data.add(char)
|
||
override fun addAll(chars: Collection<CodePoint>) = data.addAll(chars)
|
||
fun addAll(cs: CodepointSequence) = data.addAll(cs.data)
|
||
override fun removeAt(index: Int) = data.removeAt(index)
|
||
override fun retainAll(elements: Collection<CodePoint>) = data.retainAll(elements)
|
||
override fun remove(char: CodePoint) = data.remove(char)
|
||
override operator fun get(index: Int) = data[index]
|
||
override fun indexOf(element: CodePoint) = data.indexOf(element)
|
||
fun getOrNull(index: Int) = data.getOrNull(index)
|
||
fun getOrElse(index: Int, action: (Int) -> CodePoint) = data.getOrElse(index, action)
|
||
override fun isEmpty() = data.isEmpty()
|
||
override fun iterator() = data.iterator()
|
||
override fun listIterator() = data.listIterator()
|
||
override fun listIterator(index: Int) = data.listIterator()
|
||
override fun lastIndexOf(element: CodePoint) = data.lastIndexOf(element)
|
||
fun isNotEmpty() = data.isNotEmpty()
|
||
fun count(predicate: (CodePoint) -> Boolean) = data.count(predicate)
|
||
fun all(predicate: (CodePoint) -> Boolean) = data.all(predicate)
|
||
fun any(predicate: (CodePoint) -> Boolean) = data.any(predicate)
|
||
fun none(predicate: (CodePoint) -> Boolean) = data.none(predicate)
|
||
override fun contains(char: CodePoint) = data.contains(char)
|
||
fun removeIf(predicate: (CodePoint) -> Boolean) = data.removeIf(predicate)
|
||
fun spliterator() = data.spliterator()
|
||
fun stream() = data.stream()
|
||
fun parallelStream() = data.parallelStream()
|
||
override fun removeAll(elements: Collection<CodePoint>) = data.removeAll(elements)
|
||
override fun addAll(index: Int, elements: Collection<CodePoint>) = data.addAll(index, elements)
|
||
fun addAll(index: Int, elements: CodepointSequence) = data.addAll(index, elements.data)
|
||
override fun subList(fromIndex: Int, toIndex: Int) = data.subList(fromIndex, toIndex)
|
||
fun slice(indices: IntRange) = data.slice(indices)
|
||
override fun clear() = data.clear()
|
||
override fun containsAll(elements: Collection<CodePoint>) = data.containsAll(elements)
|
||
|
||
fun penultimate() = data[data.size - 2]
|
||
fun penultimateOrNull() = data.getOrNull(data.size - 2)
|
||
|
||
fun toArray() = data.toArray()
|
||
fun toList() = data.toList()
|
||
|
||
|
||
fun isGlue() = data.size == 1 && (data[0] == ZWSP || data[0] in 0xFFFE0..0xFFFFF)
|
||
fun isNotGlue() = !isGlue()
|
||
fun isZeroGlue() = data.size == 1 && (data[0] == ZWSP)
|
||
|
||
private fun CharArray.toSurrogatedString(): String = if (this.size == 1) "${this[0]}" else "${this[0]}${this[1]}"
|
||
private inline fun Int.codepointToString() = Character.toChars(this).toSurrogatedString()
|
||
private fun CodePoint.toHex() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}"
|
||
|
||
fun toHexes() = data.joinToString(" ") { it.toHex() }
|
||
|
||
fun toReadable() = data.joinToString("") {
|
||
if (it in 0x00..0x1f)
|
||
"${(0x2400 + it).toChar()}"
|
||
else if (it == 0x20 || it == 0xF0520)
|
||
"\u2423"
|
||
else if (it == NBSP)
|
||
"{NBSP}"
|
||
else if (it == SHY)
|
||
"{SHY}"
|
||
else if (it == ZWSP)
|
||
"{ZWSP}"
|
||
else if (it == OBJ)
|
||
"{OBJ:"
|
||
else if (it in FIXED_BLOCK_1..FIXED_BLOCK_1 +15)
|
||
" <block ${it - FIXED_BLOCK_1 + 1}>"
|
||
else if (it in GLUE_NEGATIVE_ONE..GLUE_POSITIVE_SIXTEEN)
|
||
" <glue ${it.glueCharToGlueSize()}> "
|
||
else if (it == 0x100000)
|
||
"{CC:null}"
|
||
else if (it in 0x10F000..0x10FFFF) {
|
||
val r = ((it and 0xF00) ushr 8).toString(16).toUpperCase()
|
||
val g = ((it and 0x0F0) ushr 4).toString(16).toUpperCase()
|
||
val b = ((it and 0x00F) ushr 0).toString(16).toUpperCase()
|
||
"{CC:#$r$g$b}"
|
||
}
|
||
else if (it in 0xFFF70..0xFFF79)
|
||
(it - 0xFFF70 + 0x30).codepointToString()
|
||
else if (it == 0xFFF7D)
|
||
"-"
|
||
else if (it in 0xFFF80..0xFFF9A)
|
||
(it - 0xFFF80 + 0x40).codepointToString()
|
||
else if (it == 0xFFF9F)
|
||
"}"
|
||
else if (it in 0xF0541..0xF055A)
|
||
(it - 0xF0541 + 0x1D670).codepointToString()
|
||
else if (it in 0xF0561..0xF057A)
|
||
(it - 0xF0561 + 0x1D68A).codepointToString()
|
||
else if (it in 0xF0530..0xF0539)
|
||
(it - 0xF0530 + 0x1D7F6).codepointToString()
|
||
else if (it in 0xF0520..0xF057F)
|
||
(it - 0xF0520 + 0x20).codepointToString()
|
||
else if (it >= 0xF0000)
|
||
it.toHex() + " "
|
||
else
|
||
Character.toString(it.toChar())
|
||
}
|
||
}
|
||
internal typealias CodePoint = Int
|
||
internal typealias ARGB8888 = Int
|
||
internal typealias Hash = Long
|
||
|
||
/**
|
||
* LibGDX port of Terrarum Sans Bitmap implementation
|
||
*
|
||
* Filename and Extension for the spritesheet is hard-coded, which are:
|
||
*
|
||
* - ascii_variable.tga
|
||
* - hangul_johab.tga
|
||
* - LatinExtA_variable.tga
|
||
* - LatinExtB_variable.tga
|
||
* - kana.tga
|
||
* - cjkpunct.tga
|
||
* - wenquanyi.tga
|
||
* - cyrillic_variable.tga
|
||
* - fullwidth_forms.tga
|
||
* - unipunct_variable.tga
|
||
* - greek_variable.tga
|
||
* - thai_variable.tga
|
||
* - puae000-e0ff.tga
|
||
*
|
||
*
|
||
* Glyphs are drawn lazily (calculated on-the-fly, rather than load up all), which is inevitable as we just can't load
|
||
* up 40k+ characters on the machine, which will certainly make loading time painfully long.
|
||
*
|
||
* Color Codes have following Unicode mapping: U+10RGBA, A must be non-zero to be visible. U+100000 reverts any colour
|
||
* code effects.
|
||
*
|
||
* ## Control Characters
|
||
*
|
||
* - U+100000: Clear colour keys
|
||
* - U+100001..U+10FFFF: Colour key (in RGBA order)
|
||
* - U+FFFC0: Charset override -- Default (incl. Russian, Ukrainian, etc.)
|
||
* - U+FFFC1: Charset override -- Bulgarian
|
||
* - U+FFFC2: Charset override -- Serbian
|
||
*
|
||
* ## Auto Shift Down
|
||
*
|
||
* Certain characters (e.g. Combining Diacritical Marks) will automatically shift down to accomodate lowercase letters.
|
||
* Shiftdown only occurs when non-diacritic character before the mark is lowercase, and the mark itself would stack up.
|
||
* Stack-up or down is defined using Tag system.
|
||
*
|
||
* @param noShadow Self-explanatory
|
||
* @param flipY If you have Y-down coord system implemented on your GDX (e.g. legacy codebase), set this to ```true```
|
||
* so that the shadow won't be upside-down.
|
||
*
|
||
* Created by minjaesong on 2017-06-15.
|
||
*/
|
||
class TerrarumSansBitmap(
|
||
val noShadow: Boolean = false,
|
||
val flipY: Boolean = false,
|
||
val invertShadow: Boolean = false,
|
||
var errorOnUnknownChar: Boolean = false,
|
||
val textCacheSize: Int = 256,
|
||
val debug: Boolean = false,
|
||
val shadowAlpha: Float = 0.5f,
|
||
val shadowAlphaPremultiply: Boolean = false
|
||
) : BitmapFont() {
|
||
|
||
private fun dbgprn(i: Any) { if (debug) println("[${this.javaClass.simpleName}] $i") }
|
||
|
||
constructor(noShadow: Boolean, flipY: Boolean, invertShadow: Boolean) : this(noShadow, flipY, invertShadow, false, 256, false)
|
||
|
||
/* This font is a collection of various subsystems, and thus contains copious amount of quick-and-dirty codes.
|
||
*
|
||
* Notable subsystems:
|
||
* - Hangul Assembler with 19 sets
|
||
* - Cyrillic Bulgarian and Serbian Variant Selectors
|
||
* - Colour Codes
|
||
* - Insular, Kana (these are relatively trivial; they just attemps to merge various Unicode sections
|
||
* into one sheet and gives custom internal indices)
|
||
*/
|
||
|
||
private var textCacheCap = 0
|
||
private val textCache = HashMap<Long, TextCacheObj>(textCacheSize * 2)
|
||
|
||
private var typesetCacheCap = 0
|
||
private val typesetCache = HashMap<Long, MovableType>(textCacheSize)
|
||
|
||
/**
|
||
* Insertion sorts the last element fo the textCache
|
||
*/
|
||
private fun addToCache(cacheObj: TextCacheObj) {
|
||
if (textCacheCap < textCacheSize * 2) {
|
||
textCache[cacheObj.hash] = cacheObj
|
||
textCacheCap += 1
|
||
}
|
||
else {
|
||
// randomly eliminate one
|
||
textCache.remove(textCache.keys.random())!!.dispose()
|
||
|
||
// add new one
|
||
textCache[cacheObj.hash] = cacheObj
|
||
}
|
||
}
|
||
|
||
private fun addToTypesetCache(cacheObj: MovableType) {
|
||
if (typesetCacheCap < textCacheSize) {
|
||
typesetCache[cacheObj.hash] = cacheObj
|
||
typesetCacheCap += 1
|
||
}
|
||
else {
|
||
// randomly eliminate one
|
||
typesetCache.remove(typesetCache.keys.random())!!.dispose()
|
||
|
||
// add new one
|
||
typesetCache[cacheObj.hash] = cacheObj
|
||
}
|
||
}
|
||
|
||
private fun getCache(hash: Long): TextCacheObj? {
|
||
return textCache[hash]
|
||
}
|
||
|
||
private fun getTypesetCache(hash: Long): MovableType? {
|
||
return typesetCache[hash]
|
||
}
|
||
|
||
|
||
private fun getColour(codePoint: Int): Int { // input: 0x10F_RGB, out: RGBA8888
|
||
if (colourBuffer.containsKey(codePoint))
|
||
return colourBuffer[codePoint]!!
|
||
|
||
val r = codePoint.and(0x0F00).ushr(8)
|
||
val g = codePoint.and(0x00F0).ushr(4)
|
||
val b = codePoint.and(0x000F)
|
||
|
||
val col = r.shl(28) or r.shl(24) or
|
||
g.shl(20) or g.shl(16) or
|
||
b.shl(12) or b.shl(8) or
|
||
0xFF
|
||
|
||
|
||
colourBuffer[codePoint] = col
|
||
return col
|
||
}
|
||
|
||
|
||
private val colourBuffer = HashMap<CodePoint, ARGB8888>()
|
||
// private val fontParentDir = if (fontDir.endsWith('/') || fontDir.endsWith('\\')) fontDir else "$fontDir/"
|
||
|
||
|
||
/** Props of all printable Unicode points. */
|
||
private val glyphProps = HashMap<CodePoint, GlyphProps>()
|
||
private val textReplaces = HashMap<CodePoint, CodePoint>()
|
||
private val sheets: Array<PixmapRegionPack>
|
||
|
||
// private var charsetOverride = 0
|
||
|
||
private val tempDir = System.getProperty("java.io.tmpdir")
|
||
// private val tempFiles = ArrayList<String>()
|
||
|
||
init {
|
||
val sheetsPack = ArrayList<PixmapRegionPack>()
|
||
|
||
// first we create pixmap to read pixels, then make texture using pixmap
|
||
fileList.forEachIndexed { index, it ->
|
||
val isVariable = it.endsWith("_variable.tga")
|
||
val isXYSwapped = it.contains("xyswap", true)
|
||
val isExtraWide = it.contains("extrawide", true)
|
||
|
||
var pixmap: Pixmap
|
||
|
||
val status = ArrayList<String>()
|
||
if (isVariable) status.add("VARIABLE")
|
||
if (isXYSwapped) status.add("XYSWAP")
|
||
if (isExtraWide) status.add("EXTRAWIDE")
|
||
|
||
if (status.size > 0)
|
||
dbgprn("loading texture [${status.joinToString()}] $it")
|
||
else
|
||
dbgprn("loading texture [STATIC] $it")
|
||
|
||
|
||
// unpack gz if applicable
|
||
/*if (it.endsWith(".gz")) {
|
||
val tmpFilePath = tempDir + "/tmp_${it.dropLast(7)}.tga"
|
||
|
||
try {
|
||
val gzi = GZIPInputStream(Gdx.files.classpath(fontParentDir + it).read(8192))
|
||
val wholeFile = gzi.readBytes()
|
||
gzi.close()
|
||
val fos = BufferedOutputStream(FileOutputStream(tmpFilePath))
|
||
fos.write(wholeFile)
|
||
fos.flush()
|
||
fos.close()
|
||
|
||
pixmap = Pixmap(Gdx.files.absolute(tmpFilePath))
|
||
// tempFiles.add(tmpFilePath)
|
||
}
|
||
catch (e: GdxRuntimeException) {
|
||
//e.printStackTrace()
|
||
dbgprn("said texture not found, skipping...")
|
||
|
||
pixmap = Pixmap(1, 1, Pixmap.Format.RGBA8888)
|
||
}
|
||
//File(tmpFileName).delete()
|
||
}
|
||
else {*/
|
||
try {
|
||
pixmap = Pixmap(Gdx.files.classpath("assets/$it"))
|
||
}
|
||
catch (e: Throwable) {
|
||
e.printStackTrace()
|
||
dbgprn("said texture not found, skipping...")
|
||
|
||
// if non-ascii chart is missing, replace it with null sheet
|
||
pixmap = Pixmap(1, 1, Pixmap.Format.RGBA8888)
|
||
// else, notify by error
|
||
if (index == 0) {
|
||
println("[${this.javaClass.simpleName}] The ASCII sheet is gone, something is wrong.")
|
||
System.exit(1)
|
||
}
|
||
}
|
||
//}
|
||
|
||
if (isVariable) buildWidthTable(pixmap, codeRange[index], if (isExtraWide) 32 else 16)
|
||
buildWidthTableFixed()
|
||
buildWidthTableInternal()
|
||
|
||
setupDynamicTextReplacer()
|
||
|
||
/*if (!noShadow) {
|
||
makeShadowForSheet(pixmap)
|
||
}*/
|
||
|
||
|
||
//val texture = Texture(pixmap)
|
||
val texRegPack = if (isExtraWide)
|
||
PixmapRegionPack(pixmap, W_WIDEVAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped)
|
||
else if (isVariable)
|
||
PixmapRegionPack(pixmap, W_VAR_INIT, H, HGAP_VAR, 0, xySwapped = isXYSwapped)
|
||
else if (index == SHEET_UNIHAN)
|
||
PixmapRegionPack(pixmap, W_UNIHAN, H_UNIHAN) // the only exception that is height is 16
|
||
// below they all have height of 20 'H'
|
||
else if (index == SHEET_HANGUL)
|
||
PixmapRegionPack(pixmap, W_HANGUL_BASE, H)
|
||
else if (index == SHEET_CUSTOM_SYM)
|
||
PixmapRegionPack(pixmap, SIZE_CUSTOM_SYM, SIZE_CUSTOM_SYM) // TODO variable
|
||
else if (index == SHEET_RUNIC)
|
||
PixmapRegionPack(pixmap, W_LATIN_WIDE, H)
|
||
else throw IllegalArgumentException("Unknown sheet index: $index")
|
||
|
||
//texRegPack.texture.setFilter(minFilter, magFilter)
|
||
|
||
sheetsPack.add(texRegPack)
|
||
|
||
pixmap.dispose() // you are terminated
|
||
}
|
||
|
||
sheets = sheetsPack.toTypedArray()
|
||
|
||
// make sure null char is actually null (draws nothing and has zero width)
|
||
sheets[SHEET_ASCII_VARW].regions[0].setColor(0)
|
||
sheets[SHEET_ASCII_VARW].regions[0].fill()
|
||
glyphProps[0] = GlyphProps(0)
|
||
}
|
||
|
||
override fun getLineHeight(): Float = LINE_HEIGHT.toFloat() * scale
|
||
override fun getXHeight() = 8f * scale
|
||
override fun getCapHeight() = 12f * scale
|
||
override fun getAscent() = 3f * scale
|
||
override fun getDescent() = 3f * scale
|
||
override fun isFlipped() = flipY
|
||
|
||
override fun setFixedWidthGlyphs(glyphs: CharSequence) {
|
||
throw UnsupportedOperationException("Nope, no monospace, and figures are already fixed width, bruv.")
|
||
}
|
||
|
||
|
||
init {
|
||
setUseIntegerPositions(true)
|
||
setOwnsTexture(true)
|
||
}
|
||
|
||
private val offsetUnihan = (H - H_UNIHAN) / 2
|
||
private val offsetCustomSym = (H - SIZE_CUSTOM_SYM) / 2
|
||
|
||
private var flagFirstRun = true
|
||
private var textBuffer = CodepointSequence()
|
||
|
||
private lateinit var tempLinotype: Texture
|
||
|
||
private var nullProp = GlyphProps(15)
|
||
|
||
fun draw(batch: Batch, charSeq: CharSequence, x: Int, y: Int) = draw(batch, charSeq, x.toFloat(), y.toFloat())
|
||
override fun draw(batch: Batch, charSeq: CharSequence, x: Float, y: Float): GlyphLayout? {
|
||
// charSeq.forEach { dbgprn("${it.toInt().charInfo()} ${glyphProps[it.toInt()]}") }
|
||
return drawNormalised(batch, charSeq.toCodePoints(), x, y)
|
||
}
|
||
fun draw(batch: Batch, codepoints: CodepointSequence, x: Int, y: Int) = drawNormalised(batch, codepoints.normalise(), x.toFloat(), y.toFloat())
|
||
fun draw(batch: Batch, codepoints: CodepointSequence, x: Float, y: Float) = drawNormalised(batch, codepoints.normalise(), x, y)
|
||
|
||
fun drawNormalised(batch: Batch, codepoints: CodepointSequence, x: Float, y: Float): GlyphLayout? {
|
||
|
||
// codepoints.forEach { dbgprn("${it.charInfo()} ${glyphProps[it]}") }
|
||
|
||
// Q&D fix for issue #12
|
||
// When the line ends with a diacritics, the whole letter won't render
|
||
// If the line starts with a letter-with-diacritic, it will error out
|
||
// Some diacritics (e.g. COMBINING TILDE) do not obey lowercase letters
|
||
val charSeqNotBlank = codepoints.size > 0 // determine emptiness BEFORE you hack a null chars in
|
||
val newCodepoints = codepoints
|
||
|
||
// always draw at integer position; this is bitmap font after all
|
||
val x = Math.round(x)
|
||
val y = Math.round(y + (lineHeight - 20 * scale) / 2)
|
||
|
||
val charSeqHash = newCodepoints.getHash()
|
||
|
||
if (charSeqNotBlank) {
|
||
|
||
var cacheObj = getCache(charSeqHash)
|
||
|
||
if (cacheObj == null || flagFirstRun) {
|
||
cacheObj = createTextCache(newCodepoints)
|
||
addToCache(cacheObj)
|
||
}
|
||
|
||
textBuffer = cacheObj.glyphLayout!!.textBuffer
|
||
tempLinotype = cacheObj.glyphLayout!!.linotype
|
||
|
||
val linotypeScaleOffsetX = -linotypePaddingX * (scale - 1)
|
||
val linotypeScaleOffsetY = -linotypePaddingY * (scale - 1) * (if (flipY) -1 else 1)
|
||
|
||
batch.draw(tempLinotype,
|
||
(x - linotypePaddingX).toFloat() + linotypeScaleOffsetX,
|
||
(y - linotypePaddingY).toFloat() + linotypeScaleOffsetY + (if (flipY) (tempLinotype.height) else 0) * scale,
|
||
tempLinotype.width.toFloat() * scale,
|
||
(tempLinotype.height.toFloat()) * (if (flipY) -1 else 1) * scale
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
fun drawToPixmap(pixmap: Pixmap, string: String, x: Int, y: Int) {
|
||
drawNormalisedToPixmap(pixmap, string.toCodePoints(), x, y)
|
||
}
|
||
|
||
fun drawNormalisedToPixmap(pixmap: Pixmap, codepoints: CodepointSequence, x: Int, y: Int) {
|
||
val charSeqNotBlank = codepoints.size > 0 // determine emptiness BEFORE you hack a null chars in
|
||
|
||
if (charSeqNotBlank) {
|
||
val (linotypePixmap, _) = createLinotypePixmap(codepoints, false)
|
||
linotypePixmap.filter = Pixmap.Filter.NearestNeighbour
|
||
|
||
val linotypeScaleOffsetX = -linotypePaddingX * (scale - 1)
|
||
val linotypeScaleOffsetY = -linotypePaddingY * (scale - 1) * (if (flipY) -1 else 1)
|
||
|
||
pixmap.drawPixmap(linotypePixmap,
|
||
0, 0, linotypePixmap.width, linotypePixmap.height,
|
||
(x - linotypePaddingX) + linotypeScaleOffsetX,
|
||
(y - linotypePaddingY) + linotypeScaleOffsetY + (if (flipY) (linotypePixmap.height) else 0) * scale,
|
||
linotypePixmap.width * scale,
|
||
(linotypePixmap.height) * (if (flipY) -1 else 1) * scale
|
||
)
|
||
|
||
linotypePixmap.dispose()
|
||
}
|
||
}
|
||
|
||
internal fun createLinotypePixmap(newCodepoints: CodepointSequence, touchTheFlag: Boolean): Pair<Pixmap, Int> {
|
||
fun Int.flipY() = this * if (flipY) 1 else -1
|
||
|
||
var renderCol = -1 // subject to change with the colour code
|
||
|
||
val textBuffer = newCodepoints
|
||
|
||
val posmap = buildPosMap(textBuffer)
|
||
|
||
if (touchTheFlag)
|
||
flagFirstRun = false
|
||
|
||
//dbgprn("text not in buffer: $charSeq")
|
||
|
||
|
||
//textBuffer.forEach { print("${it.toHex()} ") }
|
||
//dbgprn()
|
||
|
||
|
||
// resetHash(charSeq, x.toFloat(), y.toFloat())
|
||
|
||
val textWidth = posmap.width
|
||
val _pw = textWidth + (linotypePaddingX * 2)
|
||
val _ph = H + (linotypePaddingY * 2)
|
||
if (_pw < 0 || _ph < 0) throw RuntimeException("Illegal linotype dimension (w: $_pw, h: $_ph)")
|
||
val linotypePixmap = Pixmap(_pw, _ph, Pixmap.Format.RGBA8888)
|
||
|
||
|
||
var index = 0
|
||
while (index <= textBuffer.lastIndex) {
|
||
try {
|
||
var c = textBuffer[index]
|
||
val sheetID = getSheetType(c)
|
||
|
||
val (sheetX, sheetY) =
|
||
if (index == 0) getSheetwisePosition(0, c)
|
||
else getSheetwisePosition(textBuffer[index - 1], c)
|
||
val hash = getHash(c) // to be used with Bad Transmission Modifier
|
||
|
||
if (isColourCode(c)) {
|
||
if (c == 0x100000) {
|
||
renderCol = -1
|
||
}
|
||
else {
|
||
renderCol = getColour(c)
|
||
}
|
||
}
|
||
else if (sheetID == SHEET_HANGUL) {
|
||
// Flookahead for {I, P, F}
|
||
|
||
val cNext = if (index + 1 < textBuffer.size) textBuffer[index + 1] else 0
|
||
val cNextNext = if (index + 2 < textBuffer.size) textBuffer[index + 2] else 0
|
||
|
||
val hangulLength = if (isHangulJongseong(cNextNext) && isHangulJungseong(cNext))
|
||
3
|
||
else if (isHangulJungseong(cNext))
|
||
2
|
||
else
|
||
1
|
||
|
||
val (indices, rows) = toHangulIndexAndRow(c, cNext, cNextNext)
|
||
|
||
val (indexCho, indexJung, indexJong) = indices
|
||
val (choRow, jungRow, jongRow) = rows
|
||
val hangulSheet = sheets[SHEET_HANGUL]
|
||
|
||
|
||
|
||
val choTex = hangulSheet.get(indexCho, choRow)
|
||
val jungTex = hangulSheet.get(indexJung, jungRow)
|
||
val jongTex = hangulSheet.get(indexJong, jongRow)
|
||
|
||
linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
|
||
|
||
index += hangulLength - 1
|
||
|
||
}
|
||
else {
|
||
try {
|
||
val posY = posmap.y[index].flipY() +
|
||
if (sheetID == SHEET_UNIHAN) // evil exceptions
|
||
offsetUnihan
|
||
else if (sheetID == SHEET_CUSTOM_SYM)
|
||
offsetCustomSym
|
||
else 0
|
||
|
||
val posX = posmap.x[index]
|
||
val texture = sheets[sheetID].get(sheetX, sheetY)
|
||
|
||
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
|
||
|
||
|
||
}
|
||
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
|
||
}
|
||
}
|
||
|
||
|
||
index++
|
||
}
|
||
catch (e: NullPointerException) {
|
||
System.err.println("Shit hit the multithreaded fan")
|
||
e.printStackTrace()
|
||
break
|
||
}
|
||
}
|
||
|
||
|
||
makeShadow(linotypePixmap)
|
||
|
||
return linotypePixmap to textWidth
|
||
}
|
||
|
||
internal fun createTextCache(newCodepoints: CodepointSequence): TextCacheObj {
|
||
// look, I know it sounds absurd, but having this code NOT duplicated (by moving it into a separate function) will cause most of the text to turn into a black rectange
|
||
fun Int.flipY() = this * if (flipY) 1 else -1
|
||
|
||
var renderCol = -1 // subject to change with the colour code
|
||
|
||
val textBuffer = newCodepoints
|
||
|
||
val posmap = buildPosMap(textBuffer)
|
||
|
||
flagFirstRun = false
|
||
|
||
//dbgprn("text not in buffer: $charSeq")
|
||
|
||
|
||
//textBuffer.forEach { print("${it.toHex()} ") }
|
||
//dbgprn()
|
||
|
||
|
||
// resetHash(charSeq, x.toFloat(), y.toFloat())
|
||
|
||
val textWidth = posmap.width
|
||
val _pw = textWidth + (linotypePaddingX * 2)
|
||
val _ph = H + (linotypePaddingY * 2)
|
||
if (_pw < 0 || _ph < 0) throw RuntimeException("Illegal linotype dimension (w: $_pw, h: $_ph)")
|
||
val linotypePixmap = Pixmap(_pw, _ph, Pixmap.Format.RGBA8888)
|
||
|
||
|
||
var index = 0
|
||
while (index <= textBuffer.lastIndex) {
|
||
try {
|
||
var c = textBuffer[index]
|
||
val sheetID = getSheetType(c)
|
||
|
||
val (sheetX, sheetY) =
|
||
if (index == 0) getSheetwisePosition(0, c)
|
||
else getSheetwisePosition(textBuffer[index - 1], c)
|
||
val hash = getHash(c) // to be used with Bad Transmission Modifier
|
||
|
||
if (isColourCode(c)) {
|
||
if (c == 0x100000) {
|
||
renderCol = -1
|
||
}
|
||
else {
|
||
renderCol = getColour(c)
|
||
}
|
||
}
|
||
else if (sheetID == SHEET_HANGUL) {
|
||
// Flookahead for {I, P, F}
|
||
|
||
val cNext = if (index + 1 < textBuffer.size) textBuffer[index + 1] else 0
|
||
val cNextNext = if (index + 2 < textBuffer.size) textBuffer[index + 2] else 0
|
||
|
||
val hangulLength = if (isHangulJongseong(cNextNext) && isHangulJungseong(cNext))
|
||
3
|
||
else if (isHangulJungseong(cNext))
|
||
2
|
||
else
|
||
1
|
||
|
||
val (indices, rows) = toHangulIndexAndRow(c, cNext, cNextNext)
|
||
|
||
val (indexCho, indexJung, indexJong) = indices
|
||
val (choRow, jungRow, jongRow) = rows
|
||
val hangulSheet = sheets[SHEET_HANGUL]
|
||
|
||
|
||
|
||
val choTex = hangulSheet.get(indexCho, choRow)
|
||
val jungTex = hangulSheet.get(indexJung, jungRow)
|
||
val jongTex = hangulSheet.get(indexJong, jongRow)
|
||
|
||
linotypePixmap.drawPixmap(choTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
linotypePixmap.drawPixmap(jungTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
linotypePixmap.drawPixmap(jongTex, posmap.x[index] + linotypePaddingX, linotypePaddingY, renderCol)
|
||
|
||
|
||
index += hangulLength - 1
|
||
|
||
}
|
||
else {
|
||
try {
|
||
val posY = posmap.y[index].flipY() +
|
||
if (sheetID == SHEET_UNIHAN) // evil exceptions
|
||
offsetUnihan
|
||
else if (sheetID == SHEET_CUSTOM_SYM)
|
||
offsetCustomSym
|
||
else 0
|
||
|
||
val posX = posmap.x[index]
|
||
val texture = sheets[sheetID].get(sheetX, sheetY)
|
||
|
||
linotypePixmap.drawPixmap(texture, posX + linotypePaddingX, posY + linotypePaddingY, renderCol)
|
||
|
||
|
||
}
|
||
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
|
||
}
|
||
}
|
||
|
||
|
||
index++
|
||
}
|
||
catch (e: NullPointerException) {
|
||
System.err.println("Shit hit the multithreaded fan")
|
||
e.printStackTrace()
|
||
break
|
||
}
|
||
}
|
||
|
||
|
||
makeShadow(linotypePixmap)
|
||
// end of duplicated code
|
||
//val (linotypePixmap, textWidth) = createLinotypePixmap(newCodepoints, true)
|
||
|
||
val tempLinotype = Texture(linotypePixmap)
|
||
tempLinotype.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest)
|
||
|
||
// make cache object
|
||
val cacheObj = TextCacheObj(textBuffer.getHash(), ShittyGlyphLayout(textBuffer, tempLinotype, textWidth))
|
||
linotypePixmap.dispose()
|
||
|
||
return cacheObj
|
||
}
|
||
|
||
/**
|
||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||
*/
|
||
fun typesetParagraph(batch: Batch, charSeq: CharSequence, targetWidth: Int): MovableType =
|
||
typesetParagraphNormalised(batch, normaliseStringForMovableType(charSeq), targetWidth.toFloat(), TypesettingStrategy.JUSTIFIED)
|
||
/**
|
||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||
*/
|
||
fun typesetParagraph(batch: Batch, charSeq: CharSequence, targetWidth: Float): MovableType =
|
||
typesetParagraphNormalised(batch, normaliseStringForMovableType(charSeq), targetWidth, TypesettingStrategy.JUSTIFIED)
|
||
|
||
fun typesetParagraphRaggedRight(batch: Batch, charSeq: CharSequence, targetWidth: Int): MovableType =
|
||
typesetParagraphNormalised(batch, normaliseStringForMovableType(charSeq), targetWidth.toFloat(), TypesettingStrategy.RAGGED_RIGHT)
|
||
fun typesetParagraphRaggedRight(batch: Batch, charSeq: CharSequence, targetWidth: Float): MovableType =
|
||
typesetParagraphNormalised(batch, normaliseStringForMovableType(charSeq), targetWidth, TypesettingStrategy.RAGGED_RIGHT)
|
||
|
||
|
||
private val nullType = MovableType(this, "".toCodePoints(2), 0, isNull = true)
|
||
|
||
/**
|
||
* Typesets given string and returns the typesetted results, with which the desired text can be drawn on the screen.
|
||
* This method alone will NOT draw the text to the screen, use [MovableType.draw].
|
||
*/
|
||
fun typesetParagraphNormalised(batch: Batch, codepoints: CodepointSequence, targetWidth: Float, strategy: TypesettingStrategy): MovableType {
|
||
val charSeqNotBlank = codepoints.size > 0 // determine emptiness BEFORE you hack a null chars in
|
||
val newCodepoints = codepoints
|
||
|
||
val charSeqHash = newCodepoints.getHash()
|
||
|
||
if (charSeqNotBlank) {
|
||
var cacheObj = getTypesetCache(charSeqHash)
|
||
|
||
if (cacheObj == null || flagFirstRun) {
|
||
cacheObj = MovableType(this, codepoints, targetWidth.toInt(), strategy)
|
||
addToTypesetCache(cacheObj)
|
||
}
|
||
|
||
return cacheObj
|
||
}
|
||
else {
|
||
return nullType
|
||
}
|
||
}
|
||
|
||
|
||
override fun dispose() {
|
||
super.dispose()
|
||
textCache.values.forEach { it.dispose() }
|
||
sheets.forEach { it.dispose() }
|
||
}
|
||
|
||
fun getSheetType(c: CodePoint): Int {
|
||
if (isBulgarian(c))
|
||
return SHEET_BULGARIAN_VARW
|
||
else if (isSerbian(c))
|
||
return SHEET_SERBIAN_VARW
|
||
else if (isHangul(c))
|
||
return SHEET_HANGUL
|
||
else {
|
||
for (i in codeRange.indices.reversed()) {
|
||
if (c in codeRange[i]) return i
|
||
}
|
||
return SHEET_UNKNOWN
|
||
}
|
||
}
|
||
|
||
private fun getSheetwisePosition(cPrev: Int, ch: Int): IntArray {
|
||
val sheetType = getSheetType(ch)
|
||
val sheetX: Int = if (sheetType == SHEET_UNIHAN) unihanIndexX(ch) else indexX(ch)
|
||
val sheetY: Int = when (sheetType) {
|
||
SHEET_UNIHAN -> unihanIndexY(ch)
|
||
SHEET_EXTA_VARW -> extAindexY(ch)
|
||
SHEET_EXTB_VARW -> extBindexY(ch)
|
||
SHEET_KANA -> kanaIndexY(ch)
|
||
SHEET_CJK_PUNCT -> cjkPunctIndexY(ch)
|
||
SHEET_CYRILIC_VARW -> cyrilicIndexY(ch)
|
||
SHEET_HALFWIDTH_FULLWIDTH_VARW -> fullwidthUniIndexY(ch)
|
||
SHEET_UNI_PUNCT_VARW -> uniPunctIndexY(ch)
|
||
SHEET_GREEK_VARW -> greekIndexY(ch)
|
||
SHEET_THAI_VARW -> thaiIndexY(ch)
|
||
SHEET_CUSTOM_SYM -> symbolIndexY(ch)
|
||
SHEET_HAYEREN_VARW -> armenianIndexY(ch)
|
||
SHEET_KARTULI_VARW -> kartvelianIndexY(ch)
|
||
SHEET_IPA_VARW -> ipaIndexY(ch)
|
||
SHEET_RUNIC -> runicIndexY(ch)
|
||
SHEET_LATIN_EXT_ADD_VARW -> latinExtAddY(ch)
|
||
SHEET_BULGARIAN_VARW -> bulgarianIndexY(ch)
|
||
SHEET_SERBIAN_VARW -> serbianIndexY(ch)
|
||
SHEET_TSALAGI_VARW -> cherokeeIndexY(ch)
|
||
SHEET_PHONETIC_EXT_VARW -> phoneticExtIndexY(ch)
|
||
SHEET_DEVANAGARI_VARW -> devanagariIndexY(ch)
|
||
SHEET_KARTULI_CAPS_VARW -> kartvelianCapsIndexY(ch)
|
||
SHEET_DIACRITICAL_MARKS_VARW -> diacriticalMarksIndexY(ch)
|
||
SHEET_GREEK_POLY_VARW -> polytonicGreekIndexY(ch)
|
||
SHEET_EXTC_VARW -> extCIndexY(ch)
|
||
SHEET_EXTD_VARW -> extDIndexY(ch)
|
||
SHEET_CURRENCIES_VARW -> currenciesIndexY(ch)
|
||
SHEET_INTERNAL_VARW -> internalIndexY(ch)
|
||
SHEET_LETTERLIKE_MATHS_VARW -> letterlikeIndexY(ch)
|
||
SHEET_ENCLOSED_ALPHNUM_SUPL_VARW -> enclosedAlphnumSuplY(ch)
|
||
SHEET_TAMIL_VARW -> tamilIndexY(ch)
|
||
SHEET_BENGALI_VARW -> bengaliIndexY(ch)
|
||
SHEET_BRAILLE_VARW -> brailleIndexY(ch)
|
||
SHEET_SUNDANESE_VARW -> sundaneseIndexY(ch)
|
||
SHEET_DEVANAGARI2_INTERNAL_VARW -> devanagari2IndexY(ch)
|
||
SHEET_CODESTYLE_ASCII_VARW -> codestyleAsciiIndexY(ch)
|
||
SHEET_ALPHABETIC_PRESENTATION_FORMS -> alphabeticPresentationFormsY(ch)
|
||
SHEET_HENTAIGANA_VARW -> hentaiganaIndexY(ch)
|
||
else -> ch / 16
|
||
}
|
||
|
||
return intArrayOf(sheetX, sheetY)
|
||
}
|
||
|
||
private fun Boolean.toInt() = if (this) 1 else 0
|
||
/** @return THIRTY-TWO bit number: this includes alpha channel value; or 0 if alpha is zero */
|
||
private fun Int.tagify() = if (this and 255 == 0) 0 else this
|
||
|
||
/**
|
||
* Will happily overwrite any existing entries
|
||
*/
|
||
private fun buildWidthTable(pixmap: Pixmap, codeRange: Iterable<Int>, cellW: Int = 16, cols: Int = 16) {
|
||
val binaryCodeOffset = cellW - 1
|
||
|
||
val cellH = H
|
||
|
||
codeRange.forEachIndexed { index, code ->
|
||
|
||
val cellX = (index % cols) * cellW
|
||
val cellY = (index / cols) * cellH
|
||
|
||
val codeStartX = cellX + binaryCodeOffset
|
||
val codeStartY = cellY
|
||
|
||
val width = (0..4).fold(0) { acc, y -> acc or ((pixmap.getPixel(codeStartX, codeStartY + y).and(255) != 0).toInt() shl y) }
|
||
val isLowHeight = (pixmap.getPixel(codeStartX, codeStartY + 5).and(255) != 0)
|
||
|
||
// Keming machine parameters
|
||
val kerningBit1 = pixmap.getPixel(codeStartX, codeStartY + 6).tagify()
|
||
val kerningBit2 = pixmap.getPixel(codeStartX, codeStartY + 7).tagify()
|
||
val kerningBit3 = pixmap.getPixel(codeStartX, codeStartY + 8).tagify()
|
||
var isKernYtype = ((kerningBit1 and 0x80000000.toInt()) != 0)
|
||
var kerningMask = kerningBit1.ushr(8).and(0xFFFFFF)
|
||
val hasKernData = kerningBit1 and 255 != 0//(kerningBit1 and 255 != 0 && kerningMask != 0xFFFF)
|
||
if (!hasKernData) {
|
||
isKernYtype = false
|
||
kerningMask = 255
|
||
}
|
||
|
||
val compilerDirectives = pixmap.getPixel(codeStartX, codeStartY + 9).tagify()
|
||
val directiveOpcode = compilerDirectives.ushr(24).and(255)
|
||
val directiveArg1 = compilerDirectives.ushr(16).and(255)
|
||
val directiveArg2 = compilerDirectives.ushr(8).and(255)
|
||
|
||
|
||
val nudgingBits = pixmap.getPixel(codeStartX, codeStartY + 10).tagify()
|
||
val nudgeX = nudgingBits.ushr(24).toByte().toInt() // signed 8-bit int
|
||
val nudgeY = nudgingBits.ushr(16).toByte().toInt() // signed 8-bit int
|
||
|
||
val diacriticsAnchors = (0..5).map {
|
||
val yPos = 13 - (it / 3) * 2
|
||
val shift = (3 - (it % 3)) * 8
|
||
val yPixel = pixmap.getPixel(codeStartX, codeStartY + yPos).tagify()
|
||
val xPixel = pixmap.getPixel(codeStartX, codeStartY + yPos + 1).tagify()
|
||
val yUsed = (yPixel ushr shift) and 128 != 0
|
||
val xUsed = (xPixel ushr shift) and 128 != 0
|
||
val y = if (yUsed) (yPixel ushr shift) and 127 else 0
|
||
val x = if (xUsed) (xPixel ushr shift) and 127 else 0
|
||
|
||
DiacriticsAnchor(it, x, y, xUsed, yUsed)
|
||
}.toTypedArray()
|
||
|
||
val alignWhere = (0..1).fold(0) { acc, y -> acc or ((pixmap.getPixel(codeStartX, codeStartY + y + 15).and(255) != 0).toInt() shl y) }
|
||
|
||
var writeOnTop = pixmap.getPixel(codeStartX, codeStartY + 17) // NO .tagify()
|
||
if (writeOnTop and 255 == 0) writeOnTop = -1 // look for the alpha channel
|
||
else {
|
||
if (writeOnTop.ushr(8) == 0xFFFFFF) writeOnTop = 0
|
||
else writeOnTop = writeOnTop.ushr(28) and 15
|
||
}
|
||
|
||
val stackWhere0 = pixmap.getPixel(codeStartX, codeStartY + 18).tagify()
|
||
val stackWhere1 = pixmap.getPixel(codeStartX, codeStartY + 19).tagify()
|
||
|
||
val stackWhere = if (stackWhere0 == 0x00FF00FF && stackWhere1 == 0x00FF00FF)
|
||
GlyphProps.STACK_DONT
|
||
else (0..1).fold(0) { acc, y -> acc or ((pixmap.getPixel(codeStartX, codeStartY + y + 18).and(255) != 0).toInt() shl y) }
|
||
|
||
glyphProps[code] = GlyphProps(width, isLowHeight, nudgeX, nudgeY, diacriticsAnchors, alignWhere, writeOnTop, stackWhere, IntArray(15), hasKernData, isKernYtype, kerningMask, directiveOpcode, directiveArg1, directiveArg2)
|
||
|
||
// extra info
|
||
val extCount = glyphProps[code]?.requiredExtInfoCount() ?: 0
|
||
if (extCount > 0) {
|
||
|
||
for (x in 0 until extCount) {
|
||
var info = 0
|
||
for (y in 0..19) {
|
||
// if ALPHA is not zero, assume it's 1
|
||
if (pixmap.getPixel(cellX + x, cellY + y).and(255) != 0) {
|
||
info = info or (1 shl y)
|
||
}
|
||
}
|
||
|
||
glyphProps[code]!!.extInfo[x] = info
|
||
}
|
||
|
||
// println("[TerrarumSansBitmap] char with $extCount extra info: ${code.charInfo()}; opcode: ${directiveOpcode.toString(16)}")
|
||
// println("contents: ${glyphProps[code]!!.extInfo.map { it.toString(16) }.joinToString()}")
|
||
}
|
||
|
||
|
||
// Debug prints //
|
||
// dbgprn(">> ${code.charInfo()} << ")
|
||
// if (nudgingBits != 0) dbgprn("${code.charInfo()} nudgeX=$nudgeX, nudgeY=$nudgeY, nudgingBits=0x${nudgingBits.toString(16)}")
|
||
// if (writeOnTop >= 0) dbgprn("WriteOnTop: ${code.charInfo()} (Type-${writeOnTop})")
|
||
// if (diacriticsAnchors.any { it.xUsed || it.yUsed }) dbgprn("${code.charInfo()} ${diacriticsAnchors.filter { it.xUsed || it.yUsed }.joinToString()}")
|
||
// if (directiveOpcode != 0) dbgprn("Directive opcode ${directiveOpcode.toString(2)}: ${code.charInfo()}")
|
||
// if (glyphProps[code]?.isPragma("replacewith") == true) dbgprn("Replacer: ${code.charInfo()} into ${glyphProps[code]!!.extInfo.map { it.toString(16) }.joinToString()}")
|
||
// if (stackWhere == GlyphProps.STACK_DONT) dbgprn("Diacritics Don't stack: ${code.charInfo()}")
|
||
// if (stackWhere == GlyphProps.STACK_DOWN) dbgprn("Diacritics stack down: ${code.charInfo()}")
|
||
// if (writeOnTop > -1 && alignWhere == GlyphProps.ALIGN_RIGHT && width > 0) dbgprn("Diacritics aligned to the right with width of $width: ${code.charInfo()}")
|
||
// if (code in 0xF0000 until 0xF0060) dbgprn("Code ${code.toString(16)} width: $width")
|
||
}
|
||
}
|
||
|
||
private fun buildWidthTableFixed() {
|
||
// fixed-width props
|
||
codeRange[SHEET_CUSTOM_SYM].forEach { glyphProps[it] = GlyphProps(20) }
|
||
codeRange[SHEET_HANGUL].forEach { glyphProps[it] = GlyphProps(W_HANGUL_BASE) }
|
||
codeRangeHangulCompat.forEach { glyphProps[it] = GlyphProps(W_HANGUL_BASE) }
|
||
codeRange[SHEET_RUNIC].forEach { glyphProps[it] = GlyphProps(9) }
|
||
codeRange[SHEET_UNIHAN].forEach { glyphProps[it] = GlyphProps(W_UNIHAN) }
|
||
(0xD800..0xDFFF).forEach { glyphProps[it] = GlyphProps(0) }
|
||
(0x100000..0x10FFFF).forEach { glyphProps[it] = GlyphProps(0) }
|
||
(0xFFFA0..0xFFFFF).forEach { glyphProps[it] = GlyphProps(0) }
|
||
|
||
|
||
// manually add width of one orphan insular letter
|
||
// WARNING: glyphs in 0xA770..0xA778 has invalid data, further care is required
|
||
glyphProps[0x1D79] = GlyphProps(9)
|
||
|
||
|
||
// U+007F is DEL originally, but this font stores bitmap of Replacement Character (U+FFFD)
|
||
// to this position. String replacer will replace U+FFFD into U+007F.
|
||
glyphProps[0x7F] = GlyphProps(15)
|
||
|
||
}
|
||
|
||
private fun Int.halveWidth() = this / 2 + 1
|
||
|
||
private fun buildWidthTableInternal() {
|
||
for (i in 0 until 16) {
|
||
glyphProps[i] = GlyphProps(0)
|
||
glyphProps[i + 16] = GlyphProps(0)
|
||
glyphProps[FIXED_BLOCK_1 + i] = GlyphProps(i + 1)
|
||
glyphProps[MOVABLE_BLOCK_1 + i] = GlyphProps(i + 1)
|
||
glyphProps[MOVABLE_BLOCK_M1 + i] = GlyphProps(-i - 1)
|
||
}
|
||
|
||
for (i in 0 until 256) {
|
||
glyphProps[0xF800 + i] = GlyphProps(0)
|
||
}
|
||
|
||
for (i in 0xFFF70..0xFFF9F) {
|
||
glyphProps[i] = GlyphProps(0)
|
||
}
|
||
|
||
val figWidth = glyphProps[0x30]!!.width // 9 by default
|
||
val punctWidth = glyphProps[0x2E]!!.width // 6 by default
|
||
val em = 12 + 1
|
||
|
||
glyphProps[NQSP] = GlyphProps(em.halveWidth()) // 7
|
||
glyphProps[MQSP] = GlyphProps(em) // 13
|
||
glyphProps[ENSP] = GlyphProps(em.halveWidth()) // 7
|
||
glyphProps[EMSP] = GlyphProps(em) // 13
|
||
glyphProps[THREE_PER_EMSP] = GlyphProps(em / 3 + 1) // 5
|
||
glyphProps[QUARTER_EMSP] = GlyphProps(em / 4 + 1) // 4
|
||
glyphProps[SIX_PER_EMSP] = GlyphProps(em / 6 + 1) // 3
|
||
glyphProps[FSP] = GlyphProps(figWidth) // 9
|
||
glyphProps[PSP] = GlyphProps(punctWidth) // 6
|
||
glyphProps[THSP] = GlyphProps(2)
|
||
glyphProps[HSP] = GlyphProps(1)
|
||
glyphProps[ZWSP] = GlyphProps(0)
|
||
glyphProps[ZWNJ] = GlyphProps(0)
|
||
glyphProps[ZWJ] = GlyphProps(0)
|
||
|
||
glyphProps[SHY] = GlyphProps(0)
|
||
glyphProps[OBJ] = GlyphProps(0)
|
||
}
|
||
|
||
private fun setupDynamicTextReplacer() {
|
||
// replace NBSP into a block of same width
|
||
val spaceWidth = glyphProps[32]?.width ?: throw IllegalStateException()
|
||
if (spaceWidth > 16) throw InternalError("Space (U+0020) character is too wide ($spaceWidth)")
|
||
textReplaces[NBSP] = FIXED_BLOCK_1 + (spaceWidth - 1)
|
||
}
|
||
|
||
fun getWidth(text: String) = getWidthNormalised(text.toCodePoints())
|
||
fun getWidth(s: CodepointSequence) = getWidthNormalised(s.normalise())
|
||
|
||
fun getWidthNormalised(s: CodepointSequence): Int {
|
||
if (s.isEmpty())
|
||
return 0
|
||
|
||
if (s.size == 1) {
|
||
return scale * (glyphProps[s.first()]?.width ?: (
|
||
if (errorOnUnknownChar)
|
||
throw InternalError("No GlyphProps for char '${s.first().toHex()}' " +
|
||
"(${s.first().charInfo()})")
|
||
else
|
||
0
|
||
))
|
||
}
|
||
|
||
val cacheObj = getCache(s.getHash())
|
||
|
||
if (cacheObj != null) {
|
||
return cacheObj.glyphLayout!!.width * scale
|
||
}
|
||
else {
|
||
return buildPosMap(s).width * scale
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* THE function to typeset all the letters and their diacritics
|
||
*
|
||
* @return Pair of X-positions and Y-positions, of which the X-position's size is greater than the string
|
||
* and the last element marks the width of entire string.
|
||
*/
|
||
private fun buildPosMap(str: CodepointSequence): Posmap {
|
||
val posXbuffer = IntArray(str.size + 1) { 0 }
|
||
val posYbuffer = IntArray(str.size) { 0 }
|
||
|
||
|
||
var nonDiacriticCounter = 0 // index of last instance of non-diacritic char
|
||
var stackUpwardCounter = 0 // TODO separate stack counter for centre- and right aligned
|
||
var stackDownwardCounter = 0
|
||
|
||
val HALF_VAR_INIT = W_VAR_INIT.minus(1).div(2)
|
||
|
||
// this is starting to get dirty...
|
||
// persisting value. the value is set a few characters before the actual usage
|
||
var extraWidth = 0
|
||
|
||
try {
|
||
for (charIndex in 0 until posXbuffer.size - 1) {
|
||
if (charIndex > 0) {
|
||
// nonDiacriticCounter allows multiple diacritics
|
||
|
||
val thisChar = str[charIndex]
|
||
if (glyphProps[thisChar] == null && errorOnUnknownChar) {
|
||
val errorGlyphSB = StringBuilder()
|
||
Character.toChars(thisChar).forEach { errorGlyphSB.append(it) }
|
||
|
||
throw InternalError("No GlyphProps for char '$errorGlyphSB' " +
|
||
"(${thisChar.charInfo()})")
|
||
}
|
||
val thisProp = glyphProps[thisChar] ?: nullProp
|
||
val lastNonDiacriticChar = str[nonDiacriticCounter]
|
||
val itsProp = glyphProps[lastNonDiacriticChar] ?: nullProp
|
||
val kerning = getKerning(lastNonDiacriticChar, thisChar)
|
||
|
||
|
||
// if (thisChar in 0xF0000 until 0xF0060)
|
||
// dbgprn("char: ${thisChar.charInfo()}\nproperties: $thisProp")
|
||
|
||
|
||
var alignmentOffset = when (thisProp.alignWhere) {
|
||
GlyphProps.ALIGN_LEFT -> 0
|
||
GlyphProps.ALIGN_RIGHT -> thisProp.width - W_VAR_INIT
|
||
GlyphProps.ALIGN_CENTRE -> Math.ceil((thisProp.width - W_VAR_INIT) / 2.0).toInt()
|
||
else -> 0 // implies "diacriticsBeforeGlyph = true"
|
||
}
|
||
|
||
|
||
// shoehorn the wider-hangul-width thingamajig
|
||
// widen only when the next hangul char is not "jungseongWide"
|
||
// (애 in "애슬론" should not be widened)
|
||
val thisHangulJungseongIndex = toHangulJungseongIndex(thisChar)
|
||
val nextHangulJungseong1 = toHangulJungseongIndex(str.getOrNull(charIndex + 2) ?: 0) ?: -1
|
||
val nextHangulJungseong2 = toHangulJungseongIndex(str.getOrNull(charIndex + 3) ?: 0) ?: -1
|
||
if (isHangulJungseong(thisChar) && thisHangulJungseongIndex in hangulPeaksWithExtraWidth && (
|
||
nextHangulJungseong1 !in jungseongWide ||
|
||
nextHangulJungseong2 !in jungseongWide
|
||
)) {
|
||
//dbgprn("char: ${thisChar.charInfo()}\nproperties: $thisProp")
|
||
//dbgprn("${thisChar.charInfo()} ${str.getOrNull(charIndex + 2)?.charInfo()} ${str.getOrNull(charIndex + 3)?.charInfo()}")
|
||
extraWidth += 1
|
||
}
|
||
|
||
|
||
if (isHangul(thisChar) && !isHangulChoseong(thisChar) && !isHangulCompat(thisChar)) {
|
||
posXbuffer[charIndex] = posXbuffer[nonDiacriticCounter]
|
||
}
|
||
// is this glyph NOT a diacritic?
|
||
else if (thisProp.writeOnTop < 0) {
|
||
// apply interchar only if this character is NOT a control char
|
||
val thisInterchar = if (thisChar.isLetter()) interchar else 0
|
||
|
||
posXbuffer[charIndex] = -thisProp.nudgeX +
|
||
when (itsProp.alignWhere) {
|
||
GlyphProps.ALIGN_RIGHT ->
|
||
posXbuffer[nonDiacriticCounter] + W_VAR_INIT + alignmentOffset + thisInterchar + kerning + extraWidth
|
||
GlyphProps.ALIGN_CENTRE ->
|
||
posXbuffer[nonDiacriticCounter] + HALF_VAR_INIT + itsProp.width + alignmentOffset + thisInterchar + kerning + extraWidth
|
||
else ->
|
||
posXbuffer[nonDiacriticCounter] + itsProp.width + alignmentOffset + thisInterchar + kerning + extraWidth
|
||
}
|
||
posYbuffer[charIndex] = -thisProp.nudgeY
|
||
|
||
nonDiacriticCounter = charIndex
|
||
extraWidth = thisProp.nudgeX // This resets extraWidth. NOTE: sign is flipped!
|
||
|
||
stackUpwardCounter = 0
|
||
stackDownwardCounter = 0
|
||
}
|
||
// FIXME HACK: using 0th diacritics' X-anchor pos as a type selector
|
||
/*else if (thisProp.writeOnTop && thisProp.diacriticsAnchors[0].x == GlyphProps.DIA_JOINER) {
|
||
posXbuffer[charIndex] = when (itsProp.alignWhere) {
|
||
GlyphProps.ALIGN_RIGHT ->
|
||
posXbuffer[nonDiacriticCounter] + W_VAR_INIT + alignmentOffset
|
||
//GlyphProps.ALIGN_CENTRE ->
|
||
// posXbuffer[nonDiacriticCounter] + HALF_VAR_INIT + itsProp.width + alignmentOffset
|
||
else ->
|
||
posXbuffer[nonDiacriticCounter] + itsProp.width + alignmentOffset
|
||
|
||
}
|
||
}*/
|
||
// is this glyph a diacritic?
|
||
else {
|
||
val diacriticsType = thisProp.writeOnTop
|
||
// set X pos according to alignment information
|
||
posXbuffer[charIndex] = -thisProp.nudgeX +
|
||
when (thisProp.alignWhere) {
|
||
GlyphProps.ALIGN_LEFT, GlyphProps.ALIGN_BEFORE -> posXbuffer[nonDiacriticCounter]
|
||
GlyphProps.ALIGN_RIGHT -> {
|
||
// println("thisprop alignright $kerning, $extraWidth")
|
||
|
||
val anchorPoint =
|
||
if (!itsProp.diacriticsAnchors[diacriticsType].xUsed) itsProp.width else itsProp.diacriticsAnchors[diacriticsType].x
|
||
|
||
extraWidth += thisProp.width
|
||
posXbuffer[nonDiacriticCounter] + anchorPoint - W_VAR_INIT + kerning + extraWidth
|
||
}
|
||
GlyphProps.ALIGN_CENTRE -> {
|
||
val anchorPoint =
|
||
if (!itsProp.diacriticsAnchors[diacriticsType].xUsed) itsProp.width.div(2) else itsProp.diacriticsAnchors[diacriticsType].x
|
||
|
||
if (itsProp.alignWhere == GlyphProps.ALIGN_RIGHT) {
|
||
if (thisChar in 0x900..0x902)
|
||
posXbuffer[nonDiacriticCounter] + anchorPoint + (itsProp.width - 1).div(2)
|
||
else
|
||
posXbuffer[nonDiacriticCounter] + anchorPoint + (itsProp.width + 1).div(2)
|
||
} else {
|
||
if (thisChar in 0x900..0x902)
|
||
posXbuffer[nonDiacriticCounter] + anchorPoint - (W_VAR_INIT + 1) / 2
|
||
else
|
||
posXbuffer[nonDiacriticCounter] + anchorPoint - HALF_VAR_INIT
|
||
}
|
||
}
|
||
else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}")
|
||
}
|
||
|
||
// Lower Anusvara: shift right when after certain vowels or reph
|
||
if (thisChar == DEVANAGARI_ANUSVARA_LOWER) {
|
||
val prev = str.getOrElse(charIndex - 1) { -1 }
|
||
val hasSimpleReph = prev == DEVANAGARI_RA_SUPER
|
||
val hasComplexReph = prev == DEVANAGARI_RA_SUPER_COMPLEX
|
||
val hasReph = hasSimpleReph || hasComplexReph
|
||
val effectivePrev = if (hasReph) str.getOrElse(charIndex - 2) { -1 } else prev
|
||
if (effectivePrev == 0x094F || hasComplexReph) {
|
||
posXbuffer[charIndex] += 3
|
||
} else if (effectivePrev in intArrayOf(0x093A, 0x0948, 0x094C) || hasSimpleReph) {
|
||
posXbuffer[charIndex] += 2
|
||
}
|
||
}
|
||
|
||
// set Y pos according to diacritics position
|
||
when (thisProp.stackWhere) {
|
||
GlyphProps.STACK_DOWN -> {
|
||
posYbuffer[charIndex] = (-thisProp.nudgeY + H_DIACRITICS * stackDownwardCounter) * flipY.toSign()
|
||
stackDownwardCounter++
|
||
}
|
||
GlyphProps.STACK_UP -> {
|
||
posYbuffer[charIndex] = -thisProp.nudgeY + (-H_DIACRITICS * stackUpwardCounter + -thisProp.nudgeY) * flipY.toSign()
|
||
// shift down on lowercase if applicable
|
||
if (getSheetType(thisChar) in autoShiftDownOnLowercase &&
|
||
lastNonDiacriticChar.isLowHeight()) {
|
||
//dbgprn("AAARRRRHHHH for character ${thisChar.toHex()}")
|
||
//dbgprn("lastNonDiacriticChar: ${lastNonDiacriticChar.toHex()}")
|
||
//dbgprn("cond: ${thisProp.alignXPos == GlyphProps.DIA_OVERLAY}, charIndex: $charIndex")
|
||
if (diacriticsType == GlyphProps.DIA_OVERLAY)
|
||
posYbuffer[charIndex] += H_OVERLAY_LOWERCASE_SHIFTDOWN * flipY.toSign() // if minus-assign doesn't work, try plus-assign
|
||
else
|
||
posYbuffer[charIndex] += H_STACKUP_LOWERCASE_SHIFTDOWN * flipY.toSign() // if minus-assign doesn't work, try plus-assign
|
||
}
|
||
|
||
stackUpwardCounter++
|
||
|
||
// dbgprn("lastNonDiacriticChar: ${lastNonDiacriticChar.charInfo()}; stack counter: $stackUpwardCounter")
|
||
}
|
||
GlyphProps.STACK_UP_N_DOWN -> {
|
||
posYbuffer[charIndex] = (-thisProp.nudgeY + H_DIACRITICS * stackDownwardCounter) * flipY.toSign()
|
||
stackDownwardCounter++
|
||
|
||
|
||
posYbuffer[charIndex] = (-thisProp.nudgeY + -H_DIACRITICS * stackUpwardCounter) * flipY.toSign()
|
||
// shift down on lowercase if applicable
|
||
if (getSheetType(thisChar) in autoShiftDownOnLowercase &&
|
||
lastNonDiacriticChar.isLowHeight()) {
|
||
if (diacriticsType == GlyphProps.DIA_OVERLAY)
|
||
posYbuffer[charIndex] += H_OVERLAY_LOWERCASE_SHIFTDOWN * flipY.toSign() // if minus-assign doesn't work, try plus-assign
|
||
else
|
||
posYbuffer[charIndex] += H_STACKUP_LOWERCASE_SHIFTDOWN * flipY.toSign() // if minus-assign doesn't work, try plus-assign
|
||
}
|
||
|
||
stackUpwardCounter++
|
||
}
|
||
// for BEFORE_N_AFTER, do nothing in here
|
||
}
|
||
|
||
// Don't reset extraWidth here!
|
||
}
|
||
}
|
||
}
|
||
|
||
// fill the last of the posXbuffer
|
||
if (str.isNotEmpty()) {
|
||
val lastCharProp = glyphProps[str.last()]
|
||
val penultCharProp = glyphProps[str[nonDiacriticCounter]] ?:
|
||
(if (errorOnUnknownChar) throw throw InternalError("No GlyphProps for char '${str[nonDiacriticCounter]}' " +
|
||
"(${str[nonDiacriticCounter].charInfo()})") else nullProp)
|
||
posXbuffer[posXbuffer.lastIndex] = posXbuffer[posXbuffer.lastIndex - 1] + // DON'T add 1 to house the shadow, it totally breaks stuffs
|
||
if (lastCharProp != null && lastCharProp.writeOnTop >= 0) {
|
||
val realDiacriticWidth = if (lastCharProp.alignWhere == GlyphProps.ALIGN_CENTRE) {
|
||
(lastCharProp.width).div(2) + penultCharProp.diacriticsAnchors[0].x
|
||
}
|
||
else if (lastCharProp.alignWhere == GlyphProps.ALIGN_RIGHT) {
|
||
(lastCharProp.width) + penultCharProp.diacriticsAnchors[0].x
|
||
}
|
||
else 0
|
||
|
||
maxOf(penultCharProp.width, realDiacriticWidth)
|
||
}
|
||
else {
|
||
(lastCharProp?.width ?: 0)
|
||
}
|
||
}
|
||
else {
|
||
posXbuffer[0] = 0
|
||
}
|
||
}
|
||
catch (e: NullPointerException) {}
|
||
|
||
return Posmap(posXbuffer, posYbuffer)
|
||
}
|
||
|
||
private fun CodepointSequence.utf16to32(): CodepointSequence {
|
||
val seq = CodepointSequence()
|
||
|
||
var i = 0
|
||
while (i < this.size) {
|
||
val c = this[i]
|
||
|
||
// check UTF-16 surrogates
|
||
if (i < this.lastIndex && c.isHighSurrogate()) {
|
||
val cNext = this[i + 1]
|
||
|
||
if (!cNext.isLowSurrogate()) {
|
||
// replace with Unicode replacement char
|
||
seq.add(0x7F) // 0x7F in used internally to display <??> character
|
||
}
|
||
else {
|
||
val H = c
|
||
val L = cNext
|
||
|
||
seq.add(surrogatesToCodepoint(H, L))
|
||
|
||
i++ // skip next char (guaranteed to be Low Surrogate)
|
||
}
|
||
}
|
||
else {
|
||
seq.add(c)
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
return seq
|
||
}
|
||
|
||
internal fun normaliseStringForMovableType(s: CharSequence) =
|
||
s.toCodePoints(2, if (interchar == 0) 2 else 0)
|
||
|
||
// basically an Unicode NFD with some additional flavours
|
||
/**
|
||
* @param normaliseOption 1-full, 2-omit null filling
|
||
* @param ligaturesOption 0-none, 1-{ff, fi, fl, ffi, ffl, ſt, st, մն, մե, մի, վն, մխ}, 2-{ff, fi, fl, ffi, ffl, մն, մե, մի, վն, մխ}
|
||
*/
|
||
private fun CodepointSequence.normalise(normaliseOption: Int = 1, ligaturesOption: Int = 0): CodepointSequence {
|
||
val seq0 = CodepointSequence()
|
||
val seq = CodepointSequence()
|
||
val seq2 = CodepointSequence()
|
||
val seq3 = CodepointSequence()
|
||
val seq4 = CodepointSequence()
|
||
val seq5 = CodepointSequence()
|
||
val dis = this.utf16to32()
|
||
|
||
var i = 0
|
||
|
||
// dbgprn("Charsequence: ${dis.map { "${it.toChar()}${ZWNJ.toChar()}" }.joinToString(" ")}")
|
||
|
||
seq0.add(0)
|
||
while (i < dis.size) {
|
||
var c = dis[i]
|
||
val cNext = dis.getOrElse(i+1) { -1 }
|
||
|
||
// replace characters in-line
|
||
textReplaces[c]?.let {
|
||
c = it
|
||
}
|
||
|
||
// turn Unicode Devanagari consonants into the internal counterpart
|
||
if (c in 0x0915..0x0939 || c in 0x0958..0x095F)
|
||
if (cNext == DEVANAGARI_NUQTA) {
|
||
seq0.add(c.toDevaInternal().internalDevaAddNukta())
|
||
i += 1
|
||
}
|
||
else {
|
||
seq0.add(c.toDevaInternal())
|
||
}
|
||
// Contextual Visarga: use interword variant when followed by a Devanagari character
|
||
else if (c == 0x0903 && (cNext in 0x0904..0x0939 || cNext in 0x0958..0x097F)) {
|
||
seq0.add(DEVANAGARI_INTERWORD_VISARGA)
|
||
}
|
||
// re-order Sundanese diacritics
|
||
else if ((c == 0x1BA1 || c == 0x1BA2) && cNext == 0x1BA5) {
|
||
seq0.add(cNext); seq0.add(c); i += 1
|
||
}
|
||
// combine two Sundanese diacritics to internal counterpart
|
||
else if (c == 0x1BA4 && cNext == 0x1B80) { seq0.add(SUNDANESE_ING); i += 1 }
|
||
else if (c == 0x1BA8 && cNext == 0x1B80) { seq0.add(SUNDANESE_ENG); i += 1 }
|
||
else if (c == 0x1BA9 && cNext == 0x1B80) { seq0.add(SUNDANESE_EUNG); i += 1 }
|
||
else if (c == 0x1BA4 && cNext == 0x1B81) { seq0.add(SUNDANESE_IR); i += 1 }
|
||
else if (c == 0x1BA8 && cNext == 0x1B81) { seq0.add(SUNDANESE_ER); i += 1 }
|
||
else if (c == 0x1BA9 && cNext == 0x1B81) { seq0.add(SUNDANESE_EUR); i += 1 }
|
||
else if (c == 0x1BA3 && cNext == 0x1BA5) { seq0.add(SUNDANESE_LU); i += 1 }
|
||
else
|
||
seq0.add(c)
|
||
|
||
|
||
i += 1
|
||
}
|
||
seq0.add(0)
|
||
|
||
|
||
|
||
i = 0
|
||
while (i < seq0.size) {
|
||
// val cPrev2 = seq0.getOrElse(i-2) { -1 }
|
||
val cPrev = seq0.getOrElse(i-1) { -1 }
|
||
val c = seq0[i]
|
||
val cNext = seq0.getOrElse(i+1) { -1 }
|
||
val cNext2 = seq0.getOrElse(i+2) { -1 }
|
||
val cNext3 = seq0.getOrElse(i+3) { -1 }
|
||
// can't use regular sliding window as the 'i' value is changed way too often
|
||
|
||
|
||
// LET THE NORMALISATION BEGIN //
|
||
|
||
// disassemble Hangul Syllables into Initial-Peak-Final encoding
|
||
if (c in 0xAC00..0xD7A3) {
|
||
val cInt = c - 0xAC00
|
||
val indexCho = getWanseongHanChoseong(cInt)
|
||
val indexJung = getWanseongHanJungseong(cInt)
|
||
val indexJong = getWanseongHanJongseong(cInt) - 1 // no Jongseong will be -1
|
||
|
||
// these magic numbers only makes sense if you look at the Unicode chart of Hangul Jamo
|
||
// https://www.unicode.org/charts/PDF/U1100.pdf
|
||
seq.add(0x1100 + indexCho)
|
||
seq.add(0x1161 + indexJung)
|
||
if (indexJong >= 0) seq.add(0x11A8 + indexJong)
|
||
}
|
||
// normalise CJK Compatibility area because fuck them
|
||
else if (c in 0x3300..0x33FF) {
|
||
seq.add(0x7F) // fuck them
|
||
}
|
||
// add filler to malformed Hangul Initial-Peak-Final
|
||
else if (isHangul(c) && !isHangulCompat(c)) {
|
||
// possible cases
|
||
|
||
// IPF: hangul on the sequence
|
||
// i: HCF, p: HJF, x: non-hangul
|
||
// +cPrev
|
||
// v >$<: c
|
||
// I >I< -> I p I
|
||
// I >F< -> I p F
|
||
// I >x< -> I p x
|
||
|
||
// P >P< -> P i P
|
||
|
||
// F >P< -> F i P
|
||
// F >F< -> F ip F
|
||
|
||
// x >P< -> x i P
|
||
// x >F< -> x ip F
|
||
|
||
if (isHangulChoseong(cPrev) && (isHangulChoseong(c) || isHangulJongseong(c) || !isHangul(c))) {
|
||
seq.add(HJF)
|
||
}
|
||
else if (isHangulJungseong(cPrev) && isHangulJungseong(c)) {
|
||
seq.add(HCF)
|
||
}
|
||
else if (isHangulJongseong(cPrev)) {
|
||
if (isHangulJungseong(c)) seq.add(HCF)
|
||
else if (isHangulJongseong(c)) { seq.add(HCF); seq.add(HJF) }
|
||
}
|
||
else if (!isHangul(cPrev)) {
|
||
if (isHangulJungseong(c)) seq.add(HCF)
|
||
else if (isHangulJongseong(c)) { seq.add(HCF); seq.add(HJF) }
|
||
}
|
||
|
||
seq.add(c)
|
||
|
||
}
|
||
// for lowercase i and j, if cNext is a diacritic that goes on top, remove the dots
|
||
else if (diacriticDotRemoval.containsKey(c) && (glyphProps[cNext]?.writeOnTop ?: -1) >= 0 && glyphProps[cNext]?.stackWhere == GlyphProps.STACK_UP) {
|
||
seq.add(diacriticDotRemoval[c]!!)
|
||
}
|
||
|
||
// BEGIN of tamil subsystem implementation
|
||
// tamil vowel I lig
|
||
else if (c == 0xB99 && cNext == TAMIL_I) {
|
||
seq.add(0xF00F0); i++
|
||
}
|
||
else if (c == 0xBAA && cNext == TAMIL_I) {
|
||
seq.add(0xF00F1); i++
|
||
}
|
||
else if (c == 0xBAF && cNext == TAMIL_I) {
|
||
seq.add(0xF00F2); i++
|
||
}
|
||
else if (c == 0xBB2 && cNext == TAMIL_I) {
|
||
seq.add(0xF00F3); i++
|
||
}
|
||
else if (c == 0xBB5 && cNext == TAMIL_I) {
|
||
seq.add(0xF00F4); i++
|
||
}
|
||
else if (c == 0xBB8 && cNext == TAMIL_I) {
|
||
seq.add(0xF00F5); i++
|
||
}
|
||
else if (c == 0xB95 && cNext == 0xBCD && cNext2 == 0xBB7) {
|
||
seq.add(TAMIL_KSSA); i += 2
|
||
}
|
||
// there are TWO ways to represent Tamil SHRII
|
||
// https://www.unicode.org/L2/L2018/18054-tamil-shri.txt
|
||
else if (c == 0xBB6 && cNext == 0xBCD && cNext2 == 0xBB0 && cNext3 == 0xBC0) {
|
||
seq.add(TAMIL_SHRII); i += 3
|
||
}
|
||
else if (c == 0xBB8 && cNext == 0xBCD && cNext2 == 0xBB0 && cNext3 == 0xBC0) {
|
||
seq.add(TAMIL_SHRII); i += 3
|
||
}
|
||
else if (c == 0xB9F && cNext == 0xBBF) {
|
||
seq.add(0xF00C0); i++
|
||
}
|
||
else if (c == 0xB9F && cNext == 0xBC0) {
|
||
seq.add(0xF00C1); i++
|
||
}
|
||
else if (tamilLigatingConsonants.contains(c) && (cNext == 0xBC1 || cNext == 0xBC2)) {
|
||
val it = tamilLigatingConsonants.indexOf(c)
|
||
|
||
if (cNext == 0xBC1)
|
||
seq.add(0xF00C2 + it)
|
||
// dbgprn("${c.toString(16)} + ${cNext.toString(16)} replaced with ${(0xF00C2 + it).toString(16)}")
|
||
else
|
||
seq.add(0xF00D4 + it)
|
||
// dbgprn("${c.toString(16)} + ${cNext.toString(16)} replaced with ${(0xF00D4 + it).toString(16)}")
|
||
|
||
i += 1
|
||
}
|
||
// END of tamil subsystem implementation
|
||
|
||
// BEGIN of devanagari string replacer
|
||
// Alternative Forms of Cluster-initial RA
|
||
else if (c == DEVANAGARI_RA && cNext == ZWJ && cNext2 == DEVANAGARI_VIRAMA && cNext3 == DEVANAGARI_YA) {
|
||
seq.add(DEVANAGARI_RYA); i += 3
|
||
}
|
||
else if (c == DEVANAGARI_RA && cNext == ZWJ && cNext2 == DEVANAGARI_VIRAMA) {
|
||
seq.add(DEVANAGARI_RA); i += 2
|
||
}
|
||
// Unicode Devanagari Rendering Rule R14
|
||
else if (c == DEVANAGARI_RA && cNext == DEVANAGARI_U) {
|
||
seq.add(DEVANAGARI_SYLL_RU); i += 1
|
||
}
|
||
else if (c == DEVANAGARI_RA && cNext == DEVANAGARI_UU) {
|
||
seq.add(DEVANAGARI_SYLL_RUU); i += 1
|
||
}
|
||
else if (c == DEVANAGARI_RRA && cNext == DEVANAGARI_U) {
|
||
seq.add(DEVANAGARI_SYLL_RRU); i += 1
|
||
}
|
||
else if (c == DEVANAGARI_RRA && cNext == DEVANAGARI_UU) {
|
||
seq.add(DEVANAGARI_SYLL_RRUU); i += 1
|
||
}
|
||
else if (c == DEVANAGARI_HA && cNext == DEVANAGARI_U) {
|
||
seq.add(DEVANAGARI_SYLL_HU); i += 1
|
||
}
|
||
else if (c == DEVANAGARI_HA && cNext == DEVANAGARI_UU) {
|
||
seq.add(DEVANAGARI_SYLL_HUU); i += 1
|
||
}
|
||
// Unicode Devanagari Rendering Rule R2-R4
|
||
// Unicode Devanagari Rendering Rule R6-R8
|
||
// (this must precede the ligaturing-machine coded on the 2nd pass, otherwise the rules below will cause undesirable effects)
|
||
else if (devanagariConsonants.contains(c) && cNext == DEVANAGARI_VIRAMA && cNext2 == DEVANAGARI_RA) {
|
||
seq.addAll(ligateIndicConsonants(c, cNext2))
|
||
i += 2
|
||
}
|
||
// Unicode Devanagari Rendering Rule R5
|
||
else if (c == DEVANAGARI_RRA && cNext == DEVANAGARI_VIRAMA || c == DEVANAGARI_RA && cNext == DEVANAGARI_VIRAMA && cNext2 == ZWJ) {
|
||
seq.add(DEVANAGARI_EYELASH_RA)
|
||
i += 1
|
||
}
|
||
// END of devanagari string replacer
|
||
// rearrange {letter, before-and-after diacritics} as {before-diacritics, letter, after-diacritics}
|
||
else if (glyphProps[c]?.stackWhere == GlyphProps.STACK_BEFORE_N_AFTER) {
|
||
val diacriticsProp = glyphProps[c]!!
|
||
// seq.add(c) // base char is added by previous iteration, AS WE'RE LOOKING AT 'c' not 'cNext'
|
||
seq.add(diacriticsProp.extInfo[0]) // align before
|
||
seq.add(diacriticsProp.extInfo[1]) // align after
|
||
// The order may seem "wrong" but trust me it'll be corrected by the swapping code below
|
||
|
||
// dbgprn("B&A: ${cNext.charInfo()} replaced with ${diacriticsProp.extInfo[0].toString(16)} ${c.toString(16)} ${diacriticsProp.extInfo[1].toString(16)}")
|
||
}
|
||
// U+007F is DEL originally, but dis font stores bitmap of Replacement Character (U+FFFD)
|
||
// to dis position. dis line will replace U+FFFD into U+007F.
|
||
else {
|
||
// dbgprn(" nop")
|
||
|
||
seq.add(c)
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
// BEGIN of Devanagari String Replacer 2 (lookbehind type)
|
||
i = 0
|
||
while (i < seq.size) {
|
||
|
||
val cPrev2 = seq.getOrElse(i-2) { -1 }
|
||
val cPrev = seq.getOrElse(i-1) { -1 }
|
||
val c = seq[i]
|
||
|
||
// Devanagari Ligations (Lookbehind)
|
||
if (devanagariConsonants.contains(cPrev2) && cPrev == DEVANAGARI_VIRAMA && devanagariConsonants.contains(c)) {
|
||
i -= 2
|
||
|
||
repeat(3) { seq.removeAt(i) }
|
||
|
||
val ligature = ligateIndicConsonants(cPrev2, c)
|
||
ligature.forEachIndexed { index, char ->
|
||
seq.add(i + index, char)
|
||
}
|
||
|
||
i += ligature.size
|
||
}
|
||
|
||
i++
|
||
}
|
||
// END of Devanagari String Replacer 2
|
||
|
||
|
||
// second scan
|
||
i = 0
|
||
while (i < seq.size) {
|
||
// swap position of {letter, diacritics that comes before the letter}
|
||
// reposition [cluster, align-before, align-after] into [align-before, cluster, align-after]
|
||
if (i > 0 && (glyphProps[seq[i]] ?: nullProp).alignWhere == GlyphProps.ALIGN_BEFORE) {
|
||
val vowel = seq[i]
|
||
// dbgprn("Vowel realign: index $i, ${vowel.charInfo()}")
|
||
if (isDevanagari(vowel)) {
|
||
// scan for the consonant cluster backwards
|
||
// [not ligature glyphs] h h h h h c l r
|
||
var scanCounter = 1
|
||
while (true) {
|
||
val cAtCurs = seq.getOrElse(i - scanCounter) { -1 }
|
||
// dbgprn(" scan back $scanCounter, char: ${cAtCurs.charInfo()}")
|
||
if (scanCounter == 1 && devanagariConsonantsNonLig.contains(cAtCurs) ||
|
||
scanCounter > 1 && devanariConsonantsHalfs.contains(cAtCurs))
|
||
scanCounter += 1
|
||
else
|
||
break
|
||
} // scanCounter points at the terminator. the left-vowel must be placed at (i - scanCounter + 1)
|
||
|
||
seq.removeAt(i)
|
||
seq.add(i - scanCounter + 1, vowel)
|
||
}
|
||
else {
|
||
val t = seq[i - 1]
|
||
seq[i - 1] = seq[i]
|
||
seq[i] = t
|
||
}
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
|
||
// continuous ligation
|
||
i = 0
|
||
while (i < seq.size) {
|
||
|
||
val cPrev = seq.getOrElse(i-1) { -1 }
|
||
val c = seq[i]
|
||
val cNext = seq.getOrElse(i+1) { -1 }
|
||
|
||
// ligate IPA intonation graph
|
||
if (c in 0x2E5..0x2E9 && cNext in 0x2E5..0x2E9) {
|
||
seq2.add(0x200A)
|
||
seq2.add(getIntonationGraph(c-0x2E5, cNext-0x2E5))
|
||
}
|
||
else if (cPrev in 0x2E5..0x2E9 && c in 0x2E5..0x2E9 && cNext !in 0x2E5..0x2E9) {
|
||
seq2.add(0xFFE39)
|
||
}
|
||
else {
|
||
seq2.add(c)
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
|
||
// unpack replacewith
|
||
// also ligate IPA intonation graph
|
||
seq2.forEach {
|
||
if (glyphProps[it]?.isPragma("replacewith") == true) {
|
||
// dbgprn("Replacing ${it.charInfo()} into: ${glyphProps[it]!!.extInfo.map { it.toString(16) }.joinToString()}")
|
||
glyphProps[it]!!.forEachExtInfo {
|
||
if (it != 0) seq3.add(it)
|
||
}
|
||
}
|
||
else {
|
||
seq3.add(it)
|
||
}
|
||
}
|
||
|
||
// reposition devanagari RA-initials into RAsup
|
||
i = 0
|
||
dbgprnLig("seq3 = ${seq3.map { "${it.toCh()}${ZWNJ.toChar()}" }.joinToString(" ")}")
|
||
|
||
val yankedCharacters = Stack<Pair<Int, CodePoint>>() // Stack of <Position, CodePoint>; codepoint use -1 if not applicable
|
||
var yankedDevanagariRaStatus = intArrayOf(0,0) // 0: none, 1: consonants, 2: virama, 3: vowel for this syllable
|
||
var sawLeftI = false
|
||
fun changeRaStatus(n: Int) {
|
||
yankedDevanagariRaStatus[0] = yankedDevanagariRaStatus[1]
|
||
yankedDevanagariRaStatus[1] = n
|
||
}
|
||
fun resetRaStatus() {
|
||
yankedDevanagariRaStatus[0] = 0
|
||
yankedDevanagariRaStatus[1] = 0
|
||
|
||
sawLeftI = false
|
||
}
|
||
|
||
fun emptyOutYanked() {
|
||
while (!yankedCharacters.empty()) {
|
||
val poppedChar = yankedCharacters.pop()
|
||
if (poppedChar.second == DEVANAGARI_RA)
|
||
if (seq4.last() in devanagariSuperscripts || sawLeftI)
|
||
seq4.add(DEVANAGARI_RA_SUPER_COMPLEX)
|
||
else
|
||
seq4.add(DEVANAGARI_RA_SUPER)
|
||
else
|
||
seq4.add(yankedCharacters.pop().second)
|
||
}
|
||
}
|
||
while (i < seq3.size) {
|
||
val cPrev2 = seq3.getOrElse(i-2) { -1 }
|
||
val cPrev = seq3.getOrElse(i-1) { -1 }
|
||
val c = seq3[i]
|
||
val cNext = seq3.getOrElse(i+1) { -1 }
|
||
val cNext2 = seq3.getOrElse(i+2) { -1 }
|
||
|
||
dbgprnLig(" र${yankedDevanagariRaStatus[1]} Chars: ${cPrev2.toCh()}${ZWNJ.toChar()} ${cPrev.toCh()}${ZWNJ.toChar()} [ ${c.toCh()}${ZWNJ.toChar()} ] ${cNext.toCh()}${ZWNJ.toChar()} ${cNext2.toCh()}${ZWNJ.toChar()}")
|
||
|
||
|
||
// in Regex: RA vir VL* (C vir C (vir C)*)? VR* ᴿ [not V && not vir]
|
||
// TERMINATOR: right vowel | non-half consonant before any consonants
|
||
if (yankedDevanagariRaStatus[1] == 0 && c == DEVANAGARI_RA && cNext == DEVANAGARI_VIRAMA) {
|
||
dbgprnLig(" Yanking RA (0 -> 1)")
|
||
yankedCharacters.push(i to c)
|
||
changeRaStatus(1)
|
||
}
|
||
else if (yankedDevanagariRaStatus[1] == 1 && yankedDevanagariRaStatus[0] == 0 && c == DEVANAGARI_VIRAMA) {
|
||
dbgprnLig(" First Virama (1 -> 2)")
|
||
changeRaStatus(2)
|
||
}
|
||
else if (yankedDevanagariRaStatus[1] == 2 && devanagariConsonants.contains(c)) {
|
||
dbgprnLig(" Consonants after Virama (2 -> 1)")
|
||
seq4.add(c)
|
||
changeRaStatus(1)
|
||
}
|
||
else if (yankedDevanagariRaStatus[1] in listOf(1,3,5) && devanariConsonantsHalfs.contains(c)) {
|
||
dbgprnLig(" Consonants Half Form (${yankedDevanagariRaStatus[1]} -> 3)")
|
||
seq4.add(c)
|
||
changeRaStatus(3)
|
||
}
|
||
else if (yankedDevanagariRaStatus[1] == 5 && devanagariConsonants.contains(c)) {
|
||
dbgprnLig(" Consonants after Left Vowel (5 -> 1)")
|
||
seq4.add(c)
|
||
changeRaStatus(1)
|
||
|
||
if (yankedDevanagariRaStatus[1] > 0) {
|
||
dbgprnLig(" Popping out RAsup (2)")
|
||
yankedCharacters.pop()
|
||
if (seq4.last() in devanagariSuperscripts || sawLeftI)
|
||
seq4.add(DEVANAGARI_RA_SUPER_COMPLEX)
|
||
else
|
||
seq4.add(DEVANAGARI_RA_SUPER)
|
||
resetRaStatus()
|
||
}
|
||
}
|
||
else if ((yankedDevanagariRaStatus[1] > 0) && devanagariRightVowels.contains(c)) {
|
||
dbgprnLig(" Right Vowels (${yankedDevanagariRaStatus[1]} -> 4)")
|
||
seq4.add(c)
|
||
changeRaStatus(4)
|
||
}
|
||
else if ((yankedDevanagariRaStatus[1] in 1..3) && devanagariVowels.contains(c)) {
|
||
dbgprnLig(" Left Vowels (${yankedDevanagariRaStatus[1]} -> 5)")
|
||
sawLeftI = true
|
||
seq4.add(c)
|
||
changeRaStatus(5)
|
||
}
|
||
else if (yankedDevanagariRaStatus[1] > 0 && devanariConsonantsHalfs.contains(cPrev) && devanagariConsonants.contains(c)) {
|
||
dbgprnLig(" Consonant Tail after Halfs (${yankedDevanagariRaStatus[1]} -> 9)")
|
||
seq4.add(c)
|
||
changeRaStatus(9)
|
||
}
|
||
// -- termination or illegal state for Devanagari RA
|
||
else if (yankedDevanagariRaStatus[1] > 0) {
|
||
dbgprnLig(" Popping out RAsup")
|
||
yankedCharacters.pop()
|
||
if (seq4.last() in devanagariSuperscripts || sawLeftI)
|
||
seq4.add(DEVANAGARI_RA_SUPER_COMPLEX)
|
||
else
|
||
seq4.add(DEVANAGARI_RA_SUPER)
|
||
resetRaStatus()
|
||
i-- // scan this character again next time
|
||
}
|
||
else if (!isDevanagari(c) && !yankedCharacters.empty()) {
|
||
emptyOutYanked()
|
||
seq4.add(c)
|
||
resetRaStatus()
|
||
}
|
||
else {
|
||
seq4.add(c)
|
||
}
|
||
|
||
i++
|
||
}
|
||
emptyOutYanked()
|
||
|
||
seq4.add(0) // add dummy terminator
|
||
|
||
// println("seq4 = " + seq4.joinToString(" ") { it.toCh() })
|
||
|
||
// replace devanagari I/II with variants
|
||
i = 0
|
||
while (i < seq4.size) {
|
||
val cPrev = seq4.getOrElse(i - 1) { -1 }
|
||
val c = seq4[i]
|
||
|
||
if (c == DEVANAGARI_I) {
|
||
var j = 1
|
||
var w = 0
|
||
while (true) {
|
||
val cj = seq4.getOrElse(i + j) { -1 }
|
||
if (j > 3 || cj !in 0xF0140..0xF04FF)
|
||
break
|
||
|
||
if (cj in devanagariPresentationConsonants || cj in devanagariPresentationConsonantsWithRa) {
|
||
w += glyphProps[cj]?.diacriticsAnchors?.get(0)?.x ?: 0
|
||
break
|
||
}
|
||
else if (cj in devanagariPresentationConsonantsHalf || cj in devanagariPresentationConsonantsWithRaHalf) {
|
||
w += glyphProps[cj]?.width ?: 0
|
||
j += 1
|
||
}
|
||
else
|
||
break
|
||
}
|
||
|
||
// println("length: $w, consonant count: $j")
|
||
|
||
seq4[i] = (w+2).coerceIn(6,21) - 6 + 0xF0110
|
||
|
||
if (j > 1) i += j
|
||
}
|
||
else if (c == DEVANAGARI_II &&
|
||
(cPrev in devanagariPresentationConsonants || cPrev in devanagariPresentationConsonantsWithRa)) {
|
||
val w = ((glyphProps[cPrev]?.width ?: 0) - (glyphProps[cPrev]?.diacriticsAnchors?.get(0)?.x ?: 0))
|
||
|
||
// println("length: $w")
|
||
|
||
seq4[i] = 0xF012F - ((w+1).coerceIn(4,19) - 4)
|
||
}
|
||
// Contextual Anusvara: use lower variant after certain vowels/reph
|
||
else if (c == 0x0902) {
|
||
val hasReph = cPrev == DEVANAGARI_RA_SUPER || cPrev == DEVANAGARI_RA_SUPER_COMPLEX
|
||
val effectivePrev = if (hasReph) seq4.getOrElse(i - 2) { -1 } else cPrev
|
||
// 094E (prishthamatra) is reordered before the consonant cluster,
|
||
// so scan backward to find it
|
||
val hasPrishthamatra = (1..5).any { j -> seq4.getOrElse(i - j) { -1 } == 0x094E }
|
||
if (effectivePrev in intArrayOf(0x093E, 0x0948, 0x094C, 0x094F) || hasPrishthamatra || hasReph) {
|
||
seq4[i] = DEVANAGARI_ANUSVARA_LOWER
|
||
}
|
||
}
|
||
|
||
|
||
i++
|
||
}
|
||
|
||
|
||
// process charset overriding
|
||
i = 0
|
||
var charsetOverride = 0
|
||
while (i < seq4.size) {
|
||
val c = seq4[i]
|
||
val cNext = seq4.getOrNull(i + 1)
|
||
val cNext2 = seq4.getOrNull(i + 2)
|
||
if (isCharsetOverride(c))
|
||
charsetOverride = c - CHARSET_OVERRIDE_DEFAULT
|
||
else if (charsetOverride > 0) {
|
||
if (c in altCharsetCodepointDomains[charsetOverride])
|
||
seq5.add(c + altCharsetCodepointOffsets[charsetOverride])
|
||
else
|
||
seq5.add(c)
|
||
}
|
||
else {
|
||
// apply ligatures
|
||
// s-t ligs
|
||
if (ligaturesOption == 1) {
|
||
if (c == 0x17F && cNext == 0x74) {
|
||
seq5.add(0xFB05); i++
|
||
} else if (c == 0x73 && cNext == 0x74) {
|
||
seq5.add(0xFB06); i++
|
||
}
|
||
} else if (ligaturesOption > 0) {
|
||
if (c == 0x66 && cNext == 0x66 && cNext2 == 0x69) {
|
||
seq5.add(0xFB03); i += 2
|
||
} else if (c == 0x66 && cNext == 0x66 && cNext2 == 0x6C) {
|
||
seq5.add(0xFB04); i += 2
|
||
} else if (c == 0x66 && cNext == 0x66) {
|
||
seq5.add(0xFB00); i++
|
||
} else if (c == 0x66 && cNext == 0x69) {
|
||
seq5.add(0xFB01); i++
|
||
} else if (c == 0x66 && cNext == 0x6C) {
|
||
seq5.add(0xFB02); i++
|
||
} else if (c == 0x574 && cNext == 0x576) {
|
||
seq5.add(0xFB13); i++
|
||
} else if (c == 0x574 && cNext == 0x565) {
|
||
seq5.add(0xFB14); i++
|
||
} else if (c == 0x574 && cNext == 0x56B) {
|
||
seq5.add(0xFB15); i++
|
||
} else if (c == 0x57E && cNext == 0x576) {
|
||
seq5.add(0xFB16); i++
|
||
} else if (c == 0x574 && cNext == 0x56D) {
|
||
seq5.add(0xFB17); i++
|
||
}
|
||
else
|
||
seq5.add(c)
|
||
}
|
||
else
|
||
seq5.add(c)
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
// println("seq5 = " + seq5.joinToString(" ") { it.toCh() })
|
||
|
||
if (normaliseOption == 2) {
|
||
while (seq5.remove(0)) {}
|
||
return seq5
|
||
}
|
||
else {
|
||
return seq5
|
||
}
|
||
}
|
||
|
||
private fun dbgprnLig(i: Any) { if (false) println("[${this.javaClass.simpleName}] $i") }
|
||
|
||
private fun CodePoint.toCh() = if (this >= 0xF0000) this.puaToUni() else if (this < 65536) "${this.toChar()}" else this.toHex()
|
||
|
||
private val devaSyll = listOf("K","KH","G","GH","NG","C","CH","J","JH","NY","TT","TTH","DD","DDH","NN","T",
|
||
"TH","D","DH","N","NNN","P","PH","B","BH","M","Y","R","RR","L","LL","LLL",
|
||
"V","SH","SS","S","H","Q","KHH","GHH","Z","DDDH","RH","F","YY","x","x","x",
|
||
"D.R.Y","K.SS","J.NY","T.T","N.T","N.N","S.P","SS.V","SH.C","SH.N","SH.V","x","x","x","x","x",
|
||
"D.G","D.GH","D.D","D.DH","D.N","D.SS","D.BH","D.M","D.Y","D.V","mDD.DD","mDD.DDH","K.T","GH.TT","GH.TTH","GH.DDH",
|
||
"P.TT","P.TTH","P.DDH","SS.TT","SS.TTH","SS.DDH","H.NN","H.T","H.M","H.Y","H.L","H.V","x","x","x","x",
|
||
"DD.G","DD.BH","NG.G","NG.V","NG.M","CH.V","TT.TT","TT.TTH","TT.V","TTH.TTH","TTH.V","DD.DD","DD.DDH","DD.V","DDH.DDH","DDH.V"
|
||
)
|
||
// nuke this function when the time that compiled bytecode exceeds 64 kb finally arrives
|
||
private fun CodePoint.puaToUni() = when (this) {
|
||
0xF0100 -> "Ru"
|
||
0xF0101 -> "Ruu"
|
||
0xF0102 -> "RRu"
|
||
0xF0103 -> "RRuu"
|
||
0xF0104 -> "Hu"
|
||
0xF0105 -> "Huu"
|
||
0xF010B -> "ᴿᵃ"
|
||
0xF010C -> "ᴿ¹"
|
||
0xF010D -> "ᴿ²"
|
||
0xF010E -> "DDRA"
|
||
0xF010F -> "ᶴ"
|
||
0xF024C -> "Resh"
|
||
in 0xF0110..0xF011F -> "I-${(this - 0xF0110 + 1)}"
|
||
in 0xF0120..0xF012F -> "II-${(this - 0xF0120 + 1)}"
|
||
in 0xF0140 until 0xF0140+devaSyll.size -> devaSyll[this - 0xF0140]
|
||
in 0xF0230 until 0xF0230+devaSyll.size -> devaSyll[this - 0xF0230] + "ʰ"
|
||
in 0xF0320 until 0xF0320+devaSyll.size -> devaSyll[this - 0xF0320] + ".R"
|
||
in 0xF0410 until 0xF0410+devaSyll.size -> devaSyll[this - 0xF0410] + ".Rʰ"
|
||
else -> "<${this.toHex()}>"
|
||
}
|
||
|
||
|
||
/** Takes input string, do normalisation, and returns sequence of codepoints (Int)
|
||
*
|
||
* UTF-16 to ArrayList of Int. UTF-16 is because of Java
|
||
* Note: CharSequence IS a String. java.lang.String implements CharSequence.
|
||
*
|
||
* Note to Programmer: DO NOT USE CHAR LITERALS, CODE EDITORS WILL CHANGE IT TO SOMETHING ELSE !!
|
||
*
|
||
* @param normaliseOption 0-don't, 1-full, 2-omit null filling
|
||
*/
|
||
private fun CharSequence.toCodePoints(normaliseOption: Int = 1, ligaturesOption: Int = 0): CodepointSequence {
|
||
val seq = CodepointSequence()
|
||
this.forEach { seq.add(it.toInt()) }
|
||
|
||
return when (normaliseOption) {
|
||
0 -> seq
|
||
else -> seq.normalise(normaliseOption, ligaturesOption)
|
||
}
|
||
}
|
||
|
||
private fun surrogatesToCodepoint(var0: Int, var1: Int): Int {
|
||
return (var0.toInt() shl 10) + var1.toInt() + -56613888
|
||
}
|
||
|
||
|
||
/**
|
||
* Edits the given pixmap so that it would have a shadow on it.
|
||
*
|
||
* This function must be called `AFTER buildWidthTable()`
|
||
*
|
||
* The pixmap must be mutable (beware of concurrentmodificationexception).
|
||
*/
|
||
private fun makeShadowForSheet(pixmap: Pixmap) {
|
||
for (y in 0..pixmap.height - 2) {
|
||
for (x in 0..pixmap.width - 2) {
|
||
val pxNow = pixmap.getPixel(x, y) // RGBA8888
|
||
|
||
// if you have read CONTRIBUTING.md, it says the actual glyph part MUST HAVE alpha of 255.
|
||
// but some of the older spritesheets still have width tag drawn as alpha of 255.
|
||
// therefore we still skip every x+15th pixels
|
||
|
||
if (x % 16 != 15) {
|
||
if (pxNow and 0xFF == 255) {
|
||
val pxRight = (x + 1) to y
|
||
val pxBottom = x to (y + 1)
|
||
val pxBottomRight = (x + 1) to (y + 1)
|
||
val opCue = listOf(pxRight, pxBottom, pxBottomRight)
|
||
|
||
opCue.forEach {
|
||
if (pixmap.getPixel(it.first, it.second) and 0xFF == 0) {
|
||
pixmap.drawPixel(it.first, it.second,
|
||
// the shadow has the same colour, but alpha halved
|
||
pxNow.and(0xFFFFFF00.toInt()).or(0x7F)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Edits the given pixmap so that it would have a shadow on it.
|
||
*
|
||
* Meant to be used to give shadow to a linotype (typeset-finished line of pixmap)
|
||
*
|
||
* The pixmap must be mutable (beware of concurrentmodificationexception).
|
||
*/
|
||
private fun makeShadow(pixmap: Pixmap?) {
|
||
if (pixmap == null) return
|
||
|
||
pixmap.blending = Pixmap.Blending.None
|
||
|
||
// TODO when semitransparency is needed (e.g. anti-aliased)
|
||
// make three more pixmap (pixmap 2 3 4)
|
||
// translate source image over new pixmap, shift the texture to be a shadow
|
||
// draw (3, 4) -> 2, px by px s.t. only overwrites RGBA==0 pixels in 2
|
||
// for all pxs in 2, do:
|
||
// halve the alpha in 2
|
||
// draw 1 -> 2 s.t. only RGBA!=0-px-in-1 is drawn on 2; draw by overwrite
|
||
// copy 2 -> 1 px by px
|
||
// ---------------------------------------------------------------------------
|
||
// this is under assumption that new pixmap is always all zero
|
||
// it is possible the blending in the pixmap is bugged
|
||
//
|
||
// for now, no semitransparency (in colourcode && spritesheet)
|
||
|
||
val jobQueue = if (!invertShadow) arrayOf(
|
||
1 to 0,
|
||
0 to 1,
|
||
1 to 1
|
||
) else arrayOf(
|
||
-1 to 0,
|
||
0 to -1,
|
||
-1 to -1
|
||
)
|
||
|
||
jobQueue.forEach {
|
||
for (y in (if (invertShadow) 1 else 0) until (if (invertShadow) pixmap.height else pixmap.height - 1)) {
|
||
for (x in (if (invertShadow) 1 else 0) until (if (invertShadow) pixmap.width else pixmap.width - 1)) {
|
||
val pixel = pixmap.getPixel(x, y) // RGBA8888
|
||
val newPixel = pixmap.getPixel(x + it.first, y + it.second)
|
||
|
||
val newColor = Color(pixel)
|
||
newColor.a *= shadowAlpha
|
||
if (shadowAlphaPremultiply) {
|
||
newColor.r *= shadowAlpha
|
||
newColor.g *= shadowAlpha
|
||
newColor.b *= shadowAlpha
|
||
}
|
||
|
||
// in the current version, all colour-coded glyphs are guaranteed
|
||
// to be opaque
|
||
if (pixel and 0xFF == 0xFF && newPixel and 0xFF == 0) {
|
||
pixmap.drawPixel(x + it.first, y + it.second, newColor.toRGBA8888())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
/***
|
||
* @param col RGBA8888 representation
|
||
*/
|
||
private fun Pixmap.drawPixmap(pixmap: Pixmap, xPos: Int, yPos: Int, col: Int) {
|
||
for (y in 0 until pixmap.height) {
|
||
for (x in 0 until pixmap.width) {
|
||
val pixel = pixmap.getPixel(x, y) // Pixmap uses RGBA8888, while Color uses ARGB. What the fuck?
|
||
|
||
val newPixel = pixel colorTimes col
|
||
|
||
this.drawPixel(xPos + x, yPos + y, newPixel)
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun Color.toRGBA8888() =
|
||
(this.r * 255f).toInt().shl(24) or
|
||
(this.g * 255f).toInt().shl(16) or
|
||
(this.b * 255f).toInt().shl(8) or
|
||
(this.a * 255f).toInt()
|
||
|
||
/**
|
||
* RGBA8888 representation
|
||
*/
|
||
private fun Int.forceOpaque() = this.and(0xFFFFFF00.toInt()) or 0xFF
|
||
|
||
private infix fun Int.colorTimes(other: Int): Int {
|
||
val thisBytes = IntArray(4) { this.ushr(it * 8).and(255) }
|
||
val otherBytes = IntArray(4) { other.ushr(it * 8).and(255) }
|
||
|
||
return (thisBytes[0] times256 otherBytes[0]) or
|
||
(thisBytes[1] times256 otherBytes[1]).shl(8) or
|
||
(thisBytes[2] times256 otherBytes[2]).shl(16) or
|
||
(thisBytes[3] times256 otherBytes[3]).shl(24)
|
||
}
|
||
|
||
private infix fun Int.times256(other: Int) = multTable255[this][other]
|
||
|
||
private val multTable255 = Array(256) { left ->
|
||
IntArray(256) { right ->
|
||
(255f * (left / 255f).times(right / 255f)).roundToInt()
|
||
}
|
||
}
|
||
|
||
|
||
/** High surrogate comes before the low. */
|
||
private fun Char.isHighSurrogate() = (this.toInt() in 0xD800..0xDBFF)
|
||
private fun Int.isHighSurrogate() = (this.toInt() in 0xD800..0xDBFF)
|
||
/** CodePoint = 0x10000 + (H - 0xD800) * 0x400 + (L - 0xDC00) */
|
||
private fun Char.isLowSurrogate() = (this.toInt() in 0xDC00..0xDFFF)
|
||
private fun Int.isLowSurrogate() = (this.toInt() in 0xDC00..0xDFFF)
|
||
|
||
|
||
var interchar = 0
|
||
var scale = 1
|
||
set(value) {
|
||
if (value > 0) field = value
|
||
else throw IllegalArgumentException("Font scale cannot be zero or negative (input: $value)")
|
||
}
|
||
|
||
fun toColorCode(argb4444: Int): String = TerrarumSansBitmap.toColorCode(argb4444)
|
||
fun toColorCode(r: Int, g: Int, b: Int, a: Int = 0x0F): String = TerrarumSansBitmap.toColorCode(r, g, b, a)
|
||
val noColorCode = toColorCode(0x0000)
|
||
|
||
val charsetOverrideDefault = Character.toChars(CHARSET_OVERRIDE_DEFAULT).toSurrogatedString()
|
||
val charsetOverrideBulgarian = Character.toChars(CHARSET_OVERRIDE_BG_BG).toSurrogatedString()
|
||
val charsetOverrideSerbian = Character.toChars(CHARSET_OVERRIDE_SR_SR).toSurrogatedString()
|
||
val charsetOverrideCodestyle = Character.toChars(CHARSET_OVERRIDE_CODESTYLE).toSurrogatedString()
|
||
|
||
// randomiser effect hash ONLY
|
||
private val hashBasis = -3750763034362895579L
|
||
private val hashPrime = 1099511628211L
|
||
private var hashAccumulator = hashBasis
|
||
fun getHash(char: Int): Long {
|
||
hashAccumulator = hashAccumulator xor char.toLong()
|
||
hashAccumulator *= hashPrime
|
||
return hashAccumulator
|
||
}
|
||
fun resetHash(charSeq: CharSequence, x: Float, y: Float) {
|
||
hashAccumulator = hashBasis
|
||
|
||
getHash(charSeq.crc32())
|
||
getHash(x.toRawBits())
|
||
getHash(y.toRawBits())
|
||
}
|
||
|
||
|
||
|
||
private fun CharSequence.crc32(): Int {
|
||
val crc = CRC32()
|
||
this.forEach {
|
||
val it = it.toInt()
|
||
val b1 = it.shl(8).and(255)
|
||
val b2 = it.and(255)
|
||
crc.update(b1)
|
||
crc.update(b2)
|
||
}
|
||
|
||
return crc.value.toInt()
|
||
}
|
||
|
||
fun CodePoint.isLowHeight() = glyphProps[this]?.isLowheight == true
|
||
|
||
private fun getKerning(prevChar: CodePoint, thisChar: CodePoint): Int {
|
||
// dirty way of special rule sue me lol
|
||
if (lowercaseRs.contains(prevChar) && dots.contains(thisChar)) {
|
||
return -1
|
||
}
|
||
|
||
// use keming machine
|
||
val maskL = glyphProps[prevChar]?.kerningMask
|
||
val maskR = glyphProps[thisChar]?.kerningMask
|
||
return if (glyphProps[prevChar]?.hasKernData == true && glyphProps[thisChar]?.hasKernData == true) {
|
||
kerningRules.forEachIndexed { index, it ->
|
||
if (it.first.matches(maskL!!) && it.second.matches(maskR!!)) {
|
||
val contraction = if (glyphProps[prevChar]?.isKernYtype == true || glyphProps[thisChar]?.isKernYtype == true) it.yy else it.bb
|
||
|
||
// dbgprn("Kerning rule match #${index+1}: ${prevChar.toChar()}${thisChar.toChar()}, Rule:${it.first.s} ${it.second.s}; Contraction: $contraction")
|
||
|
||
return -contraction
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
else 0
|
||
}
|
||
|
||
|
||
|
||
|
||
private fun CodePoint.toHalfFormOrNull(): CodePoint? {
|
||
if (this in devanagariBaseConsonantsExtended) return null
|
||
if (this < 0xF0000) throw IllegalArgumentException("Normalise consonants to internal encoding first!")
|
||
|
||
if (this == DEVANAGARI_RYA) return DEVANAGARI_HALF_RYA
|
||
if (this == MARWARI_LIG_DD_Y) return MARWARI_HALFLIG_DD_Y
|
||
if (this == DEVANAGARI_OPEN_YA) return DEVANAGARI_OPEN_HALF_YA
|
||
|
||
(this + 240).let {
|
||
if (glyphProps[it]?.isIllegal != false)
|
||
return null
|
||
else
|
||
return it
|
||
}
|
||
}
|
||
|
||
|
||
// TODO use proper version of Virama for respective scripts
|
||
private fun CodePoint.toHalfFormOrVirama(): List<CodePoint> = this.toHalfFormOrNull().let {
|
||
// println("[TerrarumSansBitmap] toHalfForm ${this.charInfo()} = ${it?.charInfo()}")
|
||
if (it == null) listOf(this, DEVANAGARI_VIRAMA) else listOf(it)
|
||
}
|
||
|
||
|
||
// TODO use proper version of Virama for respective scripts
|
||
private fun toRaAppended(c: CodePoint): List<CodePoint> {
|
||
if (c == MARWARI_DD) return listOf(MARWARI_LIG_DD_R)
|
||
|
||
(c + 480).let {
|
||
if (glyphProps[it]?.isIllegal != false)
|
||
return listOf(c, DEVANAGARI_VIRAMA, DEVANAGARI_RA)
|
||
else
|
||
return listOf(it)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private fun ligateIndicConsonants(c1: CodePoint, c2: CodePoint, rec: Int = 0): List<CodePoint> {
|
||
// dbgprn("Indic ligation${if (rec > 0) "$rec" else ""} ${c1.toCh()} - ${c2.toCh()}")
|
||
if (c1 != DEVANAGARI_RA && c2 == DEVANAGARI_RA) return toRaAppended(c1) // Devanagari @.RA
|
||
// when the font try to ligate KSSR, the arguments are K and SSR (for some reason I don't understand).
|
||
// This method drops last Ra on c2 and then recursively ligates the remainder KSS, finally
|
||
// attaches Ra on the conjunct and returns the results.
|
||
else if (c1 != DEVANAGARI_RA && isRaAppended(c2)) {
|
||
// dbgprn("Ends with RA, trying Rlig...")
|
||
val c12WithNoRa = ligateIndicConsonants(c1, c2 - 480, rec + 1)
|
||
if (c12WithNoRa.size == 1) {
|
||
val c12andRa = toRaAppended(c12WithNoRa[0])
|
||
if (c12andRa.size == 1) {
|
||
// dbgprn("Rligation successful: ${c12WithNoRa[0].toCh()} + R = ${c12andRa[0].toCh()}")
|
||
return c12andRa
|
||
}
|
||
// dbgprn("Rligation failed: ${c12WithNoRa[0].toCh()} + R = ${c12WithNoRa.map { it.toCh() }.joinToString(" + ")}")
|
||
}
|
||
// only return when ligation is possible, otherwise let the process continue so that
|
||
// Ka-Vir-SSRa form could be returned
|
||
// else dbgprn("Ligation failed, trying ${c1.toCh()} - ${c2.toCh()}")
|
||
}
|
||
// dbgprn("continue$rec: ${c1.toCh()} - ${c2.toCh()}")
|
||
return when (c1) {
|
||
0x0915.toDevaInternal() -> /* Devanagari KA */ when (c2) {
|
||
0x0924.toDevaInternal() -> listOf(DEVANAGARI_LIG_K_T) // K.T
|
||
0x0937.toDevaInternal() -> listOf(DEVANAGARI_LIG_K_SS) // K.SS
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // K.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0918.toDevaInternal() -> /* Devanagari GHA */ when (c2) {
|
||
0x091F.toDevaInternal() -> listOf(0xF01BD) // GH.TT
|
||
0x0920.toDevaInternal() -> listOf(0xF01BE) // GH.TTH
|
||
0x0922.toDevaInternal() -> listOf(0xF01BF) // GH.DDH
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0919.toDevaInternal() -> /* Devanagari NGA */ when (c2) {
|
||
0x0928.toDevaInternal() -> listOf(0xF01CD) // NG.N
|
||
0x0915.toDevaInternal() -> listOf(0xF01CE) // NG.K
|
||
0x0916.toDevaInternal() -> listOf(0xF01CF) // NG.KH
|
||
0x0917.toDevaInternal() -> listOf(0xF01D2) // NG.G
|
||
0x0918.toDevaInternal() -> listOf(0xF01D3) // NG.GH
|
||
0x092E.toDevaInternal() -> listOf(0xF01D4) // NG.M
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // NG.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x091B.toDevaInternal() -> /* Devanagari CHA */ when (c2) {
|
||
DEVANAGARI_VA -> listOf(0xF01D5) // CH.V
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // CH.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x091C.toDevaInternal() -> /* Devanagari JA */ when (c2) {
|
||
0x091E.toDevaInternal() -> listOf(DEVANAGARI_LIG_J_NY) // J.NY
|
||
DEVANAGARI_YA -> listOf(DEVANAGARI_LIG_J_Y) // J.Y
|
||
DEVANAGARI_LIG_J_Y -> listOf(DEVANAGARI_LIG_J_J_Y) // J.J.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x091F.toDevaInternal() -> /* Devanagari TTA */ when (c2) {
|
||
0x0915.toDevaInternal() -> listOf(0xF01E0) // TT.K
|
||
0x092A.toDevaInternal() -> listOf(0xF01E1) // TT.P
|
||
0x0936.toDevaInternal() -> listOf(0xF01E2) // TT.SH
|
||
0x0938.toDevaInternal() -> listOf(0xF01E3) // TT.S
|
||
0x091F.toDevaInternal() -> listOf(0xF01D6) // TT.TT
|
||
0x0920.toDevaInternal() -> listOf(0xF01D7) // TT.TTH
|
||
DEVANAGARI_VA -> listOf(0xF01D8) // TT.V
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // TT.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0920.toDevaInternal() -> /* Devanagari TTHA */ when (c2) {
|
||
0x0920.toDevaInternal() -> listOf(0xF01D9) // TTH.TTH
|
||
DEVANAGARI_VA -> listOf(0xF01DA) // TTH.V
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // TTH.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0921.toDevaInternal() -> /* Devanagari DDA */ when (c2) {
|
||
0x0921.toDevaInternal() -> listOf(0xF01DB) // DD.DD
|
||
0x0922.toDevaInternal() -> listOf(0xF01DC) // DD.DDH
|
||
0x0917.toDevaInternal() -> listOf(0xF01D0) // DD.G
|
||
0x092D.toDevaInternal() -> listOf(0xF01D1) // DD.BH
|
||
DEVANAGARI_VA -> listOf(0xF01DD) // DD.V
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // DD.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0922.toDevaInternal() -> /* Devanagari DDHA */ when (c2) {
|
||
0x0922.toDevaInternal() -> listOf(0xF01DE) // DDH.DDH
|
||
DEVANAGARI_VA -> listOf(0xF01DF) // DDH.V
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // DDH.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0924.toDevaInternal() -> /* Devanagari TA */ when (c2) {
|
||
0x0924.toDevaInternal() -> listOf(DEVANAGARI_LIG_T_T) // T.T
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0926.toDevaInternal() -> /* Devanagari DA */ when (c2) {
|
||
0x0917.toDevaInternal() -> listOf(0xF01B0) // D.G
|
||
0x0918.toDevaInternal() -> listOf(0xF01B1) // D.GH
|
||
0x0926.toDevaInternal() -> listOf(0xF01B2) // D.D
|
||
0x0927.toDevaInternal() -> listOf(0xF01B3) // D.DH
|
||
0x0928.toDevaInternal() -> listOf(0xF01B4) // D.N
|
||
0x092C.toDevaInternal() -> listOf(0xF01B5) // D.B
|
||
0x092D.toDevaInternal() -> listOf(0xF01B6) // D.BH
|
||
0x092E.toDevaInternal() -> listOf(0xF01B7) // D.M
|
||
0x092F.toDevaInternal() -> listOf(0xF01B8) // D.Y
|
||
0x0935.toDevaInternal() -> listOf(0xF01B9) // D.V
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0928.toDevaInternal() -> /* Devanagari NA */ when (c2) {
|
||
0x0924.toDevaInternal() -> listOf(DEVANAGARI_LIG_N_T) // N.T
|
||
0x0928.toDevaInternal() -> listOf(DEVANAGARI_LIG_N_N) // N.N
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x092A.toDevaInternal() -> /* Devanagari PA */ when (c2) {
|
||
0x091F.toDevaInternal() -> listOf(0xF01C0) // P.TT
|
||
0x0920.toDevaInternal() -> listOf(0xF01C1) // P.TTH
|
||
0x0922.toDevaInternal() -> listOf(0xF01C2) // P.DDH
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0936.toDevaInternal() -> /* Devanagari SHA */ when (c2) {
|
||
0x091A.toDevaInternal() -> listOf(DEVANAGARI_LIG_SH_C) // SH.C
|
||
0x0928.toDevaInternal() -> listOf(DEVANAGARI_LIG_SH_N) // SH.N
|
||
0x0932.toDevaInternal() -> listOf(DEVANAGARI_ALT_HALF_SHA, c2) // SH.L
|
||
0x0935.toDevaInternal() -> listOf(DEVANAGARI_LIG_SH_V) // SH.V
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0937.toDevaInternal() -> /* Devanagari SSA */ when (c2) {
|
||
0x091F.toDevaInternal() -> listOf(0xF01C3) // SS.TT
|
||
0x0920.toDevaInternal() -> listOf(0xF01C4) // SS.TTH
|
||
0x0922.toDevaInternal() -> listOf(0xF01C5) // SS.DDH
|
||
0x092A.toDevaInternal() -> listOf(DEVANAGARI_LIG_SS_P) // SS.P
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0938.toDevaInternal() -> /* Devanagari SA */ when (c2) {
|
||
0x0935.toDevaInternal() -> listOf(DEVANAGARI_LIG_S_V) // S.V
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0939.toDevaInternal() -> /* Devanagari HA */ when (c2) {
|
||
0x0923.toDevaInternal() -> listOf(0xF01C6) // H.NN
|
||
0x0928.toDevaInternal() -> listOf(0xF01C7) // H.N
|
||
0x092E.toDevaInternal() -> listOf(0xF01C8) // H.M
|
||
0x092F.toDevaInternal() -> listOf(0xF01C9) // H.Y
|
||
0x0932.toDevaInternal() -> listOf(0xF01CA) // H.L
|
||
0x0935.toDevaInternal() -> listOf(0xF01CB) // H.V
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0x0978 -> /* Marwari DDA */ when (c2) {
|
||
0x0978 -> listOf(MARWARI_LIG_DD_DD) // DD.DD
|
||
0x0922.toDevaInternal() -> listOf(MARWARI_LIG_DD_DDH) // DD.DDH
|
||
DEVANAGARI_YA -> listOf(MARWARI_LIG_DD_Y) // DD.Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
0xF0331 -> /* Devanagari D.RA */ when (c2) {
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA // D.R+Y
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
in (0xF01B0..0xF01DF) + (0xF0390..0xF03BF) -> when (c2) {
|
||
DEVANAGARI_YA -> c1.toHalfFormOrVirama() + DEVANAGARI_OPEN_YA
|
||
else -> c1.toHalfFormOrVirama() + c2
|
||
}
|
||
else -> c1.toHalfFormOrVirama() + c2 // TODO use proper version of Virama for respective scripts
|
||
}
|
||
}
|
||
|
||
|
||
companion object {
|
||
|
||
internal fun CodePoint.glueCharToGlueSize() = when (this) {
|
||
ZWSP -> 0
|
||
in 0xFFFE0..0xFFFEF -> -(this - 0xFFFE0 + 1)
|
||
in 0xFFFF0..0xFFFFF -> this - 0xFFFF0 + 1
|
||
else -> throw IllegalArgumentException()
|
||
}
|
||
|
||
const internal val linotypePaddingX = 16
|
||
const internal val linotypePaddingY = 10
|
||
|
||
const val LINE_HEIGHT = 24
|
||
|
||
fun CodepointSequence.getHash(): Long {
|
||
val hashBasis = -3750763034362895579L
|
||
val hashPrime = 1099511628211L
|
||
var hashAccumulator = hashBasis
|
||
|
||
try {
|
||
this.forEach {
|
||
hashAccumulator = hashAccumulator xor it.toLong()
|
||
hashAccumulator *= hashPrime
|
||
}
|
||
}
|
||
catch (e: NullPointerException) {
|
||
System.err.println("CodepointSequence is null?!")
|
||
e.printStackTrace()
|
||
}
|
||
|
||
return hashAccumulator
|
||
}
|
||
|
||
private fun Boolean.toSign() = if (this) 1 else -1
|
||
|
||
/**
|
||
* lowercase AND the height is equal to x-height (e.g. lowercase B, D, F, H, K, L, ... does not count
|
||
*/
|
||
|
||
data class ShittyGlyphLayout(val textBuffer: CodepointSequence, val linotype: Texture, val width: Int)
|
||
data class TextCacheObj(val hash: Long, val glyphLayout: ShittyGlyphLayout?): Comparable<TextCacheObj> {
|
||
val text: CodepointSequence
|
||
get() = glyphLayout!!.textBuffer
|
||
val width: Int
|
||
get() = glyphLayout!!.width
|
||
val texture: Texture
|
||
get() = glyphLayout!!.linotype
|
||
// val penultimateChar: CodePoint
|
||
// get() = text[text.size - 2]
|
||
val penultimateCharOrNull: CodePoint?
|
||
get() = text.getOrNull(text.size - 2)
|
||
|
||
fun dispose() {
|
||
glyphLayout?.linotype?.dispose()
|
||
}
|
||
|
||
override fun compareTo(other: TextCacheObj): Int {
|
||
return (this.hash - other.hash).sign
|
||
}
|
||
}
|
||
data class Posmap(val x: IntArray, val y: IntArray) {
|
||
val width = x.maxOf { it }
|
||
}
|
||
|
||
|
||
private const val HCF = 0x115F
|
||
private const val HJF = 0x1160
|
||
|
||
internal const val JUNG_COUNT = 21
|
||
internal const val JONG_COUNT = 28
|
||
|
||
internal const val W_HANGUL_BASE = 13
|
||
internal const val W_UNIHAN = 16
|
||
internal const val W_LATIN_WIDE = 9 // width of regular letters
|
||
internal const val W_VAR_INIT = 15 // it assumes width of 15 regardless of the tagged width
|
||
internal const val W_WIDEVAR_INIT = 31 // it assumes width of 31 regardless of the tagged width
|
||
|
||
internal const val HGAP_VAR = 1
|
||
|
||
internal const val H = 20
|
||
internal const val H_UNIHAN = 16
|
||
|
||
internal const val H_DIACRITICS = 3
|
||
|
||
internal const val H_STACKUP_LOWERCASE_SHIFTDOWN = 4
|
||
internal const val H_OVERLAY_LOWERCASE_SHIFTDOWN = 2
|
||
|
||
internal const val SIZE_CUSTOM_SYM = 20
|
||
|
||
internal const val SHEET_ASCII_VARW = 0
|
||
internal const val SHEET_HANGUL = 1
|
||
internal const val SHEET_EXTA_VARW = 2
|
||
internal const val SHEET_EXTB_VARW = 3
|
||
internal const val SHEET_KANA = 4
|
||
internal const val SHEET_CJK_PUNCT = 5
|
||
internal const val SHEET_UNIHAN = 6
|
||
internal const val SHEET_CYRILIC_VARW = 7
|
||
internal const val SHEET_HALFWIDTH_FULLWIDTH_VARW = 8
|
||
internal const val SHEET_UNI_PUNCT_VARW = 9
|
||
internal const val SHEET_GREEK_VARW = 10
|
||
internal const val SHEET_THAI_VARW = 11
|
||
internal const val SHEET_HAYEREN_VARW = 12
|
||
internal const val SHEET_KARTULI_VARW = 13
|
||
internal const val SHEET_IPA_VARW = 14
|
||
internal const val SHEET_RUNIC = 15
|
||
internal const val SHEET_LATIN_EXT_ADD_VARW= 16
|
||
internal const val SHEET_CUSTOM_SYM = 17
|
||
internal const val SHEET_BULGARIAN_VARW = 18
|
||
internal const val SHEET_SERBIAN_VARW = 19
|
||
internal const val SHEET_TSALAGI_VARW = 20
|
||
internal const val SHEET_PHONETIC_EXT_VARW = 21
|
||
internal const val SHEET_DEVANAGARI_VARW=22
|
||
internal const val SHEET_KARTULI_CAPS_VARW = 23
|
||
internal const val SHEET_DIACRITICAL_MARKS_VARW = 24
|
||
internal const val SHEET_GREEK_POLY_VARW = 25
|
||
internal const val SHEET_EXTC_VARW = 26
|
||
internal const val SHEET_EXTD_VARW = 27
|
||
internal const val SHEET_CURRENCIES_VARW = 28
|
||
internal const val SHEET_INTERNAL_VARW = 29
|
||
internal const val SHEET_LETTERLIKE_MATHS_VARW = 30
|
||
internal const val SHEET_ENCLOSED_ALPHNUM_SUPL_VARW = 31
|
||
internal const val SHEET_TAMIL_VARW = 32
|
||
internal const val SHEET_BENGALI_VARW = 33
|
||
internal const val SHEET_BRAILLE_VARW = 34
|
||
internal const val SHEET_SUNDANESE_VARW = 35
|
||
internal const val SHEET_DEVANAGARI2_INTERNAL_VARW = 36
|
||
internal const val SHEET_CODESTYLE_ASCII_VARW = 37
|
||
internal const val SHEET_ALPHABETIC_PRESENTATION_FORMS = 38
|
||
internal const val SHEET_HENTAIGANA_VARW = 39
|
||
|
||
internal const val SHEET_UNKNOWN = 254
|
||
|
||
// custom codepoints
|
||
|
||
internal const val RICH_TEXT_MODIFIER_RUBY_MASTER = 0xFFFA0
|
||
internal const val RICH_TEXT_MODIFIER_RUBY_SLAVE = 0xFFFA1
|
||
internal const val RICH_TEXT_MODIFIER_SUPERSCRIPT = 0xFFFA2
|
||
internal const val RICH_TEXT_MODIFIER_SUBSCRIPT = 0xFFFA3
|
||
internal const val RICH_TEXT_MODIFIER_TAG_END = 0xFFFBF
|
||
|
||
internal const val CHARSET_OVERRIDE_DEFAULT = 0xFFFC0
|
||
internal const val CHARSET_OVERRIDE_BG_BG = 0xFFFC1
|
||
internal const val CHARSET_OVERRIDE_SR_SR = 0xFFFC2
|
||
internal const val CHARSET_OVERRIDE_CODESTYLE = 0xFFFC3
|
||
|
||
const val FIXED_BLOCK_1 = 0xFFFD0
|
||
const val MOVABLE_BLOCK_M1 = 0xFFFE0
|
||
const val MOVABLE_BLOCK_1 = 0xFFFF0
|
||
|
||
|
||
private val autoShiftDownOnLowercase = arrayOf(
|
||
SHEET_DIACRITICAL_MARKS_VARW
|
||
)
|
||
|
||
private val fileList = arrayOf( // MUST BE MATCHING WITH SHEET INDICES!!
|
||
"ascii_variable.tga",
|
||
"hangul_johab.tga",
|
||
"latinExtA_variable.tga",
|
||
"latinExtB_variable.tga",
|
||
"kana_variable.tga",
|
||
"cjkpunct_variable.tga",
|
||
"wenquanyi.tga",
|
||
"cyrilic_variable.tga",
|
||
"halfwidth_fullwidth_variable.tga",
|
||
"unipunct_variable.tga",
|
||
"greek_variable.tga",
|
||
"thai_variable.tga",
|
||
"hayeren_variable.tga",
|
||
"kartuli_variable.tga",
|
||
"ipa_ext_variable.tga",
|
||
"futhark.tga",
|
||
"latinExt_additional_variable.tga",
|
||
"puae000-e0ff.tga",
|
||
"cyrilic_bulgarian_variable.tga",
|
||
"cyrilic_serbian_variable.tga",
|
||
"tsalagi_variable.tga",
|
||
"phonetic_extensions_variable.tga",
|
||
"devanagari_variable.tga",
|
||
"kartuli_allcaps_variable.tga",
|
||
"diacritical_marks_variable.tga",
|
||
"greek_polytonic_xyswap_variable.tga",
|
||
"latinExtC_variable.tga",
|
||
"latinExtD_variable.tga",
|
||
"currencies_variable.tga",
|
||
"internal_variable.tga",
|
||
"letterlike_symbols_variable.tga",
|
||
"enclosed_alphanumeric_supplement_variable.tga",
|
||
"tamil_extrawide_variable.tga",
|
||
"bengali_variable.tga",
|
||
"braille_variable.tga",
|
||
"sundanese_variable.tga",
|
||
"devanagari_internal_extrawide_variable.tga",
|
||
"pua_codestyle_ascii_variable.tga",
|
||
"alphabetic_presentation_forms_extrawide_variable.tga",
|
||
"hentaigana_variable.tga",
|
||
)
|
||
internal val codeRange = arrayOf( // MUST BE MATCHING WITH SHEET INDICES!!
|
||
0..0xFF, // SHEET_ASCII_VARW
|
||
(0x1100..0x11FF) + (0xA960..0xA97F) + (0xD7B0..0xD7FF), // SHEET_HANGUL, because Hangul Syllables are disassembled prior to the render
|
||
0x100..0x17F, // SHEET_EXTA_VARW
|
||
0x180..0x24F, // SHEET_EXTB_VARW
|
||
(0x3040..0x30FF) + (0x31F0..0x31FF), // SHEET_KANA
|
||
0x3000..0x303F, // SHEET_CJK_PUNCT
|
||
0x3400..0x9FFF, // SHEET_UNIHAN
|
||
0x400..0x52F, // SHEET_CYRILIC_VARW
|
||
0xFF00..0xFFFF, // SHEET_HALFWIDTH_FULLWIDTH_VARW
|
||
0x2000..0x209F, // SHEET_UNI_PUNCT_VARW
|
||
0x370..0x3CE, // SHEET_GREEK_VARW
|
||
0xE00..0xE5F, // SHEET_THAI_VARW
|
||
0x530..0x58F, // SHEET_HAYEREN_VARW
|
||
0x10D0..0x10FF, // SHEET_KARTULI_VARW
|
||
0x250..0x2FF, // SHEET_IPA_VARW
|
||
0x16A0..0x16FF, // SHEET_RUNIC
|
||
0x1E00..0x1EFF, // SHEET_LATIN_EXT_ADD_VARW
|
||
0xE000..0xE0FF, // SHEET_CUSTOM_SYM
|
||
0xF0000..0xF005F, // SHEET_BULGARIAN_VARW; assign them to PUA
|
||
0xF0060..0xF00BF, // SHEET_SERBIAN_VARW; assign them to PUA
|
||
0x13A0..0x13F5, // SHEET_TSALAGI_VARW
|
||
0x1D00..0x1DBF, // SHEET_PHONETIC_EXT_VARW
|
||
(0x900..0x97F) + (0xF0100..0xF04FF), // SHEET_DEVANAGARI_VARW
|
||
0x1C90..0x1CBF, // SHEET_KARTULI_CAPS_VARW
|
||
0x300..0x36F, // SHEET_DIACRITICAL_MARKS_VARW
|
||
0x1F00..0x1FFF, // SHEET_GREEK_POLY_VARW
|
||
0x2C60..0x2C7F, // SHEET_EXTC_VARW
|
||
0xA720..0xA7FF, // SHEET_EXTD_VARW
|
||
0x20A0..0x20CF, // SHEET_CURRENCIES_VARW
|
||
0xFFE00..0xFFF9F, // SHEET_INTERNAL_VARW
|
||
0x2100..0x214F, // SHEET_LETTERLIKE_MATHS_VARW
|
||
0x1F100..0x1F1FF, // SHEET_ENCLOSED_ALPHNUM_SUPL_VARW
|
||
(0x0B80..0x0BFF) + (0xF00C0..0xF00FF), // SHEET_TAMIL_VARW
|
||
0x980..0x9FF, // SHEET_BENGALI_VARW
|
||
0x2800..0x28FF, // SHEET_BRAILLE_VARW
|
||
(0x1B80..0x1BBF) + (0x1CC0..0x1CCF) + (0xF0500..0xF050F), // SHEET_SUNDANESE_VARW
|
||
0xF0110..0xF012F, // SHEET_DEVANAGARI2_INTERNAL_VARW
|
||
0xF0520..0xF057F, // SHEET_CODESTYLE_ASCII_VARW
|
||
0xFB00..0xFB17, // SHEET_ALPHABETIC_PRESENTATION_FORMS
|
||
0x1B000..0x1B16F, // SHEET_HENTAIGANA_VARW
|
||
)
|
||
private val codeRangeHangulCompat = 0x3130..0x318F
|
||
|
||
private val altCharsetCodepointOffsets = arrayOf(
|
||
0, // null
|
||
0xF0000 - 0x400, // bulgarian
|
||
0xF0060 - 0x400, // serbian
|
||
0xF0520 - 0x20, // codestyle
|
||
)
|
||
|
||
private val altCharsetCodepointDomains = arrayOf(
|
||
0..0x10FFFF,
|
||
0x400..0x45F,
|
||
0x400..0x45F,
|
||
0x20..0x7F,
|
||
)
|
||
|
||
private val diacriticDotRemoval = hashMapOf(
|
||
'i'.toInt() to 0x131,
|
||
'j'.toInt() to 0x237
|
||
)
|
||
|
||
internal fun Int.charInfo() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}: ${Character.getName(this)}"
|
||
|
||
const val NQSP = 0x2000
|
||
const val MQSP = 0x2001
|
||
const val ENSP = 0x2002
|
||
const val EMSP = 0x2003
|
||
const val THREE_PER_EMSP = 0x2004
|
||
const val QUARTER_EMSP = 0x2005
|
||
const val SIX_PER_EMSP = 0x2006
|
||
const val FSP = 0x2007
|
||
const val PSP = 0x2008
|
||
const val THSP = 0x2009
|
||
const val HSP = 0x200A
|
||
const val ZWSP = 0x200B
|
||
const val ZWNJ = 0x200C
|
||
const val ZWJ = 0x200D
|
||
const val SHY = 0xAD
|
||
const val NBSP = 0xA0
|
||
const val OBJ = 0xFFFC
|
||
|
||
private val tamilLigatingConsonants = listOf('க','ங','ச','ஞ','ட','ண','த','ந','ன','ப','ம','ய','ர','ற','ல','ள','ழ','வ').map { it.toInt() }.toIntArray() // this is the only thing that .indexOf() is called against, so NO HASHSET
|
||
private const val TAMIL_KSSA = 0xF00ED
|
||
private const val TAMIL_SHRII = 0xF00EE
|
||
private const val TAMIL_I = 0xBBF
|
||
|
||
|
||
private const val DEVANAGARI_VIRAMA = 0x94D
|
||
private const val DEVANAGARI_NUQTA = 0x93C
|
||
private val DEVANAGARI_RA = 0x930.toDevaInternal()
|
||
private val DEVANAGARI_YA = 0x92F.toDevaInternal()
|
||
private val DEVANAGARI_RRA = 0x931.toDevaInternal()
|
||
private val DEVANAGARI_VA = 0x0935.toDevaInternal()
|
||
private val DEVANAGARI_HA = 0x939.toDevaInternal()
|
||
private const val DEVANAGARI_U = 0x941
|
||
private const val DEVANAGARI_UU = 0x942
|
||
private const val DEVANAGARI_I = 0x093F
|
||
private const val DEVANAGARI_II = 0x0940
|
||
private const val DEVANAGARI_RYA = 0xF0106
|
||
private const val DEVANAGARI_HALF_RYA = 0xF0107
|
||
|
||
private const val DEVANAGARI_SYLL_RU = 0xF0100
|
||
private const val DEVANAGARI_SYLL_RUU = 0xF0101
|
||
private const val DEVANAGARI_SYLL_RRU = 0xF0102
|
||
private const val DEVANAGARI_SYLL_RRUU = 0xF0103
|
||
private const val DEVANAGARI_SYLL_HU = 0xF0104
|
||
private const val DEVANAGARI_SYLL_HUU = 0xF0105
|
||
|
||
private const val DEVANAGARI_OPEN_YA = 0xF0108
|
||
private const val DEVANAGARI_OPEN_HALF_YA = 0xF0109
|
||
private const val DEVANAGARI_ALT_HALF_SHA = 0xF010F
|
||
private const val DEVANAGARI_EYELASH_RA = 0xF010B
|
||
private const val DEVANAGARI_RA_SUPER = 0xF010C
|
||
private const val DEVANAGARI_RA_SUPER_COMPLEX = 0xF010D
|
||
private const val DEVANAGARI_INTERWORD_VISARGA = 0xF010A
|
||
|
||
|
||
private const val MARWARI_DD = 0x978
|
||
|
||
private const val DEVANAGARI_LIG_K_T = 0xF01BC
|
||
// private const val DEVANAGARI_LIG_D_R_Y = 0xF01A0
|
||
private const val DEVANAGARI_LIG_K_SS = 0xF01A1
|
||
private const val DEVANAGARI_LIG_J_NY = 0xF01A2
|
||
private const val DEVANAGARI_LIG_T_T = 0xF01A3
|
||
private const val DEVANAGARI_LIG_N_T = 0xF01A4
|
||
private const val DEVANAGARI_LIG_N_N = 0xF01A5
|
||
private const val DEVANAGARI_LIG_S_V = 0xF01A6
|
||
private const val DEVANAGARI_LIG_SS_P = 0xF01A7
|
||
private const val DEVANAGARI_LIG_SH_C = 0xF01A8
|
||
private const val DEVANAGARI_LIG_SH_N = 0xF01A9
|
||
private const val DEVANAGARI_LIG_SH_V = 0xF01AA
|
||
private const val DEVANAGARI_LIG_J_Y = 0xF01AB
|
||
private const val DEVANAGARI_LIG_J_J_Y = 0xF01AC
|
||
|
||
private const val MARWARI_LIG_DD_DD = 0xF01BA
|
||
private const val MARWARI_LIG_DD_DDH = 0xF01BB
|
||
// F016D is assigned as MARWARI_HALF_DD, referenced by compiler directives for MARWARI_LIG_DD_Y and MARWARI_HALFLIG_DD_Y
|
||
private const val MARWARI_LIG_DD_Y = 0xF016E
|
||
private const val MARWARI_HALFLIG_DD_Y = 0xF016F
|
||
private const val MARWARI_LIG_DD_R = 0xF010E
|
||
|
||
private const val DEVANAGARI_ANUSVARA_LOWER = 0xF016C
|
||
|
||
private const val SUNDANESE_ING = 0xF0500
|
||
private const val SUNDANESE_ENG = 0xF0501
|
||
private const val SUNDANESE_EUNG = 0xF0502
|
||
private const val SUNDANESE_IR = 0xF0503
|
||
private const val SUNDANESE_ER = 0xF0504
|
||
private const val SUNDANESE_EUR = 0xF0505
|
||
private const val SUNDANESE_LU = 0xF0506
|
||
|
||
|
||
private val devanagariConsonants = ((0x0915..0x0939) + (0x0958..0x095F) + (0x0978..0x097F) +
|
||
(0xF0140..0xF04FF) + (0xF0106..0xF0109)).toHashSet()
|
||
private val devanagariVowels = ((0x093A..0x093C) + (0x093E..0x094C) + (0x094E..0x094F)).toHashSet()
|
||
private val devanagariRightVowels = ((0x093A..0x093C) + 0x093E + (0x0940..0x094C) + 0x094F).toHashSet()
|
||
|
||
private val devanagariBaseConsonants = 0x0915..0x0939
|
||
private val devanagariBaseConsonantsWithNukta = 0x0958..0x095F
|
||
private val devanagariBaseConsonantsExtended = 0x0978..0x097F
|
||
private val devanagariPresentationConsonants = 0xF0140..0xF022F
|
||
private val devanagariPresentationConsonantsHalf = 0xF0230..0xF031F
|
||
private val devanagariPresentationConsonantsWithRa = 0xF0320..0xF040F
|
||
private val devanagariPresentationConsonantsWithRaHalf = 0xF0410..0xF04FF
|
||
|
||
private val devanagariSuperscripts = ((0x0900..0x0902) + (0x093A..0x093B) + 0x0940 + (0x0945..0x094C) + 0x094F + 0x0951 + (0x0953..0x0955)).toHashSet()
|
||
|
||
private val devanagariConsonantsNonLig = (devanagariBaseConsonants +
|
||
devanagariBaseConsonantsWithNukta + devanagariBaseConsonantsExtended +
|
||
devanagariPresentationConsonants + devanagariPresentationConsonantsWithRa).toHashSet()
|
||
|
||
private val devanariConsonantsHalfs = (devanagariPresentationConsonantsHalf +
|
||
devanagariPresentationConsonantsWithRaHalf + listOf(DEVANAGARI_HALF_RYA, DEVANAGARI_OPEN_HALF_YA)).toHashSet()
|
||
|
||
private fun Int.internalDevaAddNukta(): Int {
|
||
return this + 48
|
||
}
|
||
|
||
private val devanagariUnicodeNuqtaTable = intArrayOf(0xF0170,0xF0171,0xF0172,0xF0177,0xF017C,0xF017D,0xF0186,0xF018A)
|
||
|
||
private fun Int.toDevaInternal(): Int {
|
||
if (this in 0x0915..0x0939) return this - 0x0915 + 0xF0140
|
||
else if (this in 0x0958..0x095F) return devanagariUnicodeNuqtaTable[this - 0x0958]
|
||
else throw IllegalArgumentException("No Internal form exists for ${this.charInfo()}")
|
||
}
|
||
|
||
private fun isRaAppended(c: CodePoint) = c in (0xF0320..0xF04FF)
|
||
|
||
/**
|
||
* @param tone1 0..4 where 0 is extra-high
|
||
*/
|
||
private fun getIntonationGraph(tone1: Int, tone2: Int): CodePoint {
|
||
return 0xFFE20 + tone1 * 5 + tone2
|
||
}
|
||
|
||
// If this letter is a candidate to be influenced by the interchar property (i.e. is this character visible or whitespace and not a control character)
|
||
fun CodePoint.isLetter() = (Character.isLetterOrDigit(this) || Character.isWhitespace(this) || this in 0xF0000 until 0xFFF70)
|
||
|
||
private fun Int.toHex() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}"
|
||
|
||
// Hangul Implementation Specific //
|
||
|
||
private fun getWanseongHanChoseong(hanIndex: Int) = hanIndex / (JUNG_COUNT * JONG_COUNT)
|
||
private fun getWanseongHanJungseong(hanIndex: Int) = hanIndex / JONG_COUNT % JUNG_COUNT
|
||
private fun getWanseongHanJongseong(hanIndex: Int) = hanIndex % JONG_COUNT
|
||
|
||
// THESE ARRAYS MUST BE SORTED
|
||
// ㅣ
|
||
private val jungseongI = arrayOf(21,61).toSortedSet()
|
||
// ㅗ ㅛ ㅜ ㅠ
|
||
private val jungseongOU = arrayOf(9,13,14,18,34,35,39,45,51,53,54,64,73,80,83).toSortedSet()
|
||
// ㅘ ㅙ ㅞ
|
||
private val jungseongOUComplex = (arrayOf(10,11,16) + (22..33).toList() + arrayOf(36,37,38) + (41..44).toList() + (46..50).toList() + (56..59).toList() + arrayOf(63) + (67..72).toList() + (74..79).toList() + (81..83).toList() + (85..91).toList() + arrayOf(93, 94)).toSortedSet()
|
||
// ㅐ ㅒ ㅔ ㅖ etc
|
||
private val jungseongRightie = arrayOf(2,4,6,8,11,16,32,33,37,42,44,48,50,71,72,75,78,79,83,86,87,88,94).toSortedSet()
|
||
// ㅚ *ㅝ* ㅟ
|
||
private val jungseongOEWI = arrayOf(12,15,17,40,52,55,89,90,91).toSortedSet()
|
||
// ㅡ
|
||
private val jungseongEU = arrayOf(19,62,66).toSortedSet()
|
||
// ㅢ
|
||
private val jungseongYI = arrayOf(20,60,65).toSortedSet()
|
||
// ㅜ ㅝ ㅞ ㅟ ㅠ
|
||
private val jungseongUU = arrayOf(14,15,16,17,18,27,30,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,59,67,68,73,77,78,79,80,81,82,83,84,91).toSortedSet()
|
||
|
||
private val jungseongWide = (jungseongOU.toList() + jungseongEU.toList()).toSortedSet()
|
||
|
||
private val choseongGiyeoks = arrayOf(0,1,15,23,30,34,45,51,56,65,82,90,100,101,110,111,115).toSortedSet()
|
||
|
||
// index of the peak, 0 being blank, 1 being ㅏ
|
||
// indices of peaks that number of lit pixels (vertically counted) on x=11 is greater than 7
|
||
private val hangulPeaksWithExtraWidth = arrayOf(2,4,6,8,11,16,32,33,37,42,44,48,50,71,75,78,79,83,86,87,88,94).toSortedSet()
|
||
|
||
private val giyeokRemapping = hashMapOf(
|
||
5 to 19,
|
||
6 to 20,
|
||
7 to 21,
|
||
8 to 22,
|
||
11 to 23,
|
||
12 to 24,
|
||
)
|
||
|
||
/**
|
||
* @param i Initial (Choseong)
|
||
* @param p Peak (Jungseong)
|
||
* @param f Final (Jongseong)
|
||
*/
|
||
private fun getHanInitialRow(i: Int, p: Int, f: Int): Int {
|
||
var ret =
|
||
if (p in jungseongI) 3
|
||
else if (p in jungseongOEWI) 11
|
||
else if (p in jungseongOUComplex) 7
|
||
else if (p in jungseongOU) 5
|
||
else if (p in jungseongEU) 9
|
||
else if (p in jungseongYI) 13
|
||
else 1
|
||
|
||
if (f != 0) ret += 1
|
||
//println("getHanInitialRow $i $p $f -> $ret")
|
||
return if (p in jungseongUU && i in choseongGiyeoks) giyeokRemapping[ret] ?: throw NullPointerException("i=$i p=$p f=$f ret=$ret") else ret
|
||
}
|
||
|
||
private fun getHanMedialRow(i: Int, p: Int, f: Int) = if (f == 0) 15 else 16
|
||
|
||
private fun getHanFinalRow(i: Int, p: Int, f: Int): Int {
|
||
|
||
return if (p !in jungseongRightie)
|
||
17
|
||
else
|
||
18
|
||
}
|
||
|
||
private fun isHangulChoseong(c: CodePoint) = c in (0x1100..0x115F) || c in (0xA960..0xA97F)
|
||
private fun isHangulJungseong(c: CodePoint) = c in (0x1160..0x11A7) || c in (0xD7B0..0xD7C6)
|
||
private fun isHangulJongseong(c: CodePoint) = c in (0x11A8..0x11FF) || c in (0xD7CB..0xD7FB)
|
||
|
||
private fun toHangulChoseongIndex(c: CodePoint) =
|
||
if (!isHangulChoseong(c)) throw IllegalArgumentException("This Hangul sequence does not begin with Choseong (${c.toHex()})")
|
||
else if (c in 0x1100..0x115F) c - 0x1100
|
||
else c - 0xA960 + 96
|
||
private fun toHangulJungseongIndex(c: CodePoint) =
|
||
if (!isHangulJungseong(c)) null
|
||
else if (c in 0x1160..0x11A7) c - 0x1160
|
||
else c - 0xD7B0 + 72
|
||
private fun toHangulJongseongIndex(c: CodePoint) =
|
||
if (!isHangulJongseong(c)) null
|
||
else if (c in 0x11A8..0x11FF) c - 0x11A8 + 1
|
||
else c - 0xD7CB + 88 + 1
|
||
|
||
/**
|
||
* X-position in the spritesheet
|
||
*
|
||
* @param iCP Code point for Initial (Choseong)
|
||
* @param pCP Code point for Peak (Jungseong)
|
||
* @param fCP Code point for Final (Jongseong)
|
||
*/
|
||
private fun toHangulIndex(iCP: CodePoint, pCP: CodePoint, fCP: CodePoint): IntArray {
|
||
val indexI = toHangulChoseongIndex(iCP)
|
||
val indexP = toHangulJungseongIndex(pCP) ?: 0
|
||
val indexF = toHangulJongseongIndex(fCP) ?: 0
|
||
|
||
return intArrayOf(indexI, indexP, indexF)
|
||
}
|
||
|
||
/**
|
||
* @param iCP 0x1100..0x115F, 0xA960..0xA97F, 0x3130..0x318F
|
||
* @param pCP 0x00, 0x1160..0x11A7, 0xD7B0..0xD7CA
|
||
* @param fCP 0x00, 0x11A8..0x11FF, 0xD7BB..0xD7FF
|
||
*
|
||
* @return IntArray pair representing Hangul indices and rows (in this order)
|
||
*/
|
||
private fun toHangulIndexAndRow(iCP: CodePoint, pCP: CodePoint, fCP: CodePoint): Pair<IntArray, IntArray> {
|
||
if (isHangulCompat(iCP)) {
|
||
return intArrayOf(iCP - 0x3130, 0, 0) to intArrayOf(0, 15, 17)
|
||
}
|
||
else {
|
||
val (indexI, indexP, indexF) = toHangulIndex(iCP, pCP, fCP)
|
||
|
||
val rowI = getHanInitialRow(indexI, indexP, indexF)
|
||
val rowP = getHanMedialRow(indexI, indexP, indexF)
|
||
val rowF = getHanFinalRow(indexI, indexP, indexF)
|
||
|
||
return intArrayOf(indexI, indexP, indexF) to intArrayOf(rowI, rowP, rowF)
|
||
}
|
||
}
|
||
|
||
|
||
// END Hangul //
|
||
|
||
// latin specific //
|
||
|
||
private val lowercaseRs = arrayOf(0x72, 0x155, 0x157, 0x159, 0x211, 0x213, 0x27c, 0x1e59, 0x1e58, 0x1e5f).toSortedSet()
|
||
private val dots = arrayOf(0x2c, 0x2e).toSortedSet()
|
||
|
||
// END latin //
|
||
|
||
private fun isHangul(c: CodePoint) = c in codeRange[SHEET_HANGUL] || c in codeRangeHangulCompat
|
||
private fun isBulgarian(c: CodePoint) = c in 0xF0000..0xF005F
|
||
private fun isSerbian(c: CodePoint) = c in 0xF0060..0xF00BF
|
||
fun isColourCode(c: CodePoint) = c == 0x100000 || c in 0x10F000..0x10FFFF
|
||
private fun isCharsetOverride(c: CodePoint) = c in 0xFFFC0..0xFFFCF
|
||
private fun isDevanagari(c: CodePoint) = c in codeRange[SHEET_DEVANAGARI_VARW]
|
||
private fun isHangulCompat(c: CodePoint) = c in codeRangeHangulCompat
|
||
|
||
|
||
private fun indexX(c: CodePoint) = c % 16
|
||
private fun unihanIndexX(c: CodePoint) = (c - 0x3400) % 256
|
||
|
||
private fun extAindexY(c: CodePoint) = (c - 0x100) / 16
|
||
private fun extBindexY(c: CodePoint) = (c - 0x180) / 16
|
||
private fun runicIndexY(c: CodePoint) = (c - 0x16A0) / 16
|
||
private fun kanaIndexY(c: CodePoint) =
|
||
if (c in 0x31F0..0x31FF) 12
|
||
else (c - 0x3040) / 16
|
||
private fun cjkPunctIndexY(c: CodePoint) = (c - 0x3000) / 16
|
||
private fun cyrilicIndexY(c: CodePoint) = (c - 0x400) / 16
|
||
private fun bulgarianIndexY(c: CodePoint) = (c - 0xF0000) / 16
|
||
private fun serbianIndexY(c: CodePoint) = (c - 0xF0060) / 16
|
||
private fun fullwidthUniIndexY(c: CodePoint) = (c - 0xFF00) / 16
|
||
private fun uniPunctIndexY(c: CodePoint) = (c - 0x2000) / 16
|
||
private fun unihanIndexY(c: CodePoint) = (c - 0x3400) / 256
|
||
private fun greekIndexY(c: CodePoint) = (c - 0x370) / 16
|
||
private fun thaiIndexY(c: CodePoint) = (c - 0xE00) / 16
|
||
private fun symbolIndexY(c: CodePoint) = (c - 0xE000) / 16
|
||
private fun armenianIndexY(c: CodePoint) = (c - 0x530) / 16
|
||
private fun kartvelianIndexY(c: CodePoint) = (c - 0x10D0) / 16
|
||
private fun ipaIndexY(c: CodePoint) = (c - 0x250) / 16
|
||
private fun latinExtAddY(c: CodePoint) = (c - 0x1E00) / 16
|
||
private fun cherokeeIndexY(c: CodePoint) = (c - 0x13A0) / 16
|
||
private fun phoneticExtIndexY(c: CodePoint) = (c - 0x1D00) / 16
|
||
private fun devanagariIndexY(c: CodePoint) = (if (c < 0xF0000) (c - 0x0900) else (c - 0xF0080)) / 16
|
||
private fun bengaliIndexY(c: CodePoint) = (c - 0x980) / 16
|
||
private fun kartvelianCapsIndexY(c: CodePoint) = (c - 0x1C90) / 16
|
||
private fun diacriticalMarksIndexY(c: CodePoint) = (c - 0x300) / 16
|
||
private fun polytonicGreekIndexY(c: CodePoint) = (c - 0x1F00) / 16
|
||
private fun extCIndexY(c: CodePoint) = (c - 0x2C60) / 16
|
||
private fun extDIndexY(c: CodePoint) = (c - 0xA720) / 16
|
||
private fun currenciesIndexY(c: CodePoint) = (c - 0x20A0) / 16
|
||
private fun internalIndexY(c: CodePoint) = (c - 0xFFE00) / 16
|
||
private fun letterlikeIndexY(c: CodePoint) = (c - 0x2100) / 16
|
||
private fun enclosedAlphnumSuplY(c: CodePoint) = (c - 0x1F100) / 16
|
||
private fun tamilIndexY(c: CodePoint) = (if (c < 0xF0000) (c - 0x0B80) else (c - 0xF0040)) / 16
|
||
private fun brailleIndexY(c: CodePoint) = (c - 0x2800) / 16
|
||
private fun sundaneseIndexY(c: CodePoint) = (if (c >= 0xF0500) (c - 0xF04B0) else if (c < 0x1BC0) (c - 0x1B80) else (c - 0x1C80)) / 16
|
||
private fun devanagari2IndexY(c: CodePoint) = (c - 0xF0110) / 16
|
||
private fun codestyleAsciiIndexY(c: CodePoint) = (c - 0xF0520) / 16
|
||
private fun alphabeticPresentationFormsY(c: CodePoint) = (c - 0xFB00) / 16
|
||
private fun hentaiganaIndexY(c: CodePoint) = (c - 0x1B000) / 16
|
||
|
||
val charsetOverrideDefault = Character.toChars(CHARSET_OVERRIDE_DEFAULT).toSurrogatedString()
|
||
val charsetOverrideBulgarian = Character.toChars(CHARSET_OVERRIDE_BG_BG).toSurrogatedString()
|
||
val charsetOverrideSerbian = Character.toChars(CHARSET_OVERRIDE_SR_SR).toSurrogatedString()
|
||
val charsetOverrideCodestyle = Character.toChars(CHARSET_OVERRIDE_CODESTYLE).toSurrogatedString()
|
||
fun toColorCode(argb4444: Int): String = Character.toChars(0x100000 + argb4444).toSurrogatedString()
|
||
fun toColorCode(r: Int, g: Int, b: Int, a: Int = 0x0F): String = toColorCode(a.shl(12) or r.shl(8) or g.shl(4) or b)
|
||
private fun CharArray.toSurrogatedString(): String = "${this[0]}${this[1]}"
|
||
|
||
val noColorCode = toColorCode(0x0000)
|
||
|
||
|
||
|
||
// The Keming Machine //
|
||
|
||
private val kemingBitMask: IntArray = intArrayOf(7,6,5,4,3,2,1,0,15,14).map { 1 shl it }.toIntArray()
|
||
|
||
private class ing(val s: String) {
|
||
|
||
private var careBits = 0
|
||
private var ruleBits = 0
|
||
|
||
init {
|
||
s.forEachIndexed { index, char ->
|
||
when (char) {
|
||
'@' -> {
|
||
careBits = careBits or kemingBitMask[index]
|
||
ruleBits = ruleBits or kemingBitMask[index]
|
||
}
|
||
'`' -> {
|
||
careBits = careBits or kemingBitMask[index]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun matches(shapeBits: Int) = ((shapeBits and careBits) == ruleBits)
|
||
|
||
override fun toString() = "C:${careBits.toString(2).padStart(16,'0')}-R:${ruleBits.toString(2).padStart(16,'0')}"
|
||
}
|
||
|
||
private data class Kem(val first: ing, val second: ing, val bb: Int = 2, val yy: Int = 1)
|
||
|
||
/**
|
||
* Legend: _ dont care
|
||
* @ must have a bit set
|
||
* ` must have a bit unset
|
||
* Order: ABCDEFGHJK, where
|
||
*
|
||
* A·B < unset for lowheight miniscules, as in e
|
||
* |·| < space we don't care
|
||
* C·D < middle hole for majuscules, as in C
|
||
* E·F < middle hole for miniscules, as in c
|
||
* G·H
|
||
*――― < baseline
|
||
* |·|
|
||
* J·K
|
||
*/
|
||
private val kerningRules = arrayListOf(
|
||
Kem(ing("_`_@___`__"),ing("`_`___@___")), // ул
|
||
Kem(ing("_@_`___`__"),ing("`_________")),
|
||
Kem(ing("_@_@___`__"),ing("`___@_@___"),1,1),
|
||
Kem(ing("_@_@_`_`__"),ing("`_____@___")),
|
||
Kem(ing("___`_`____"),ing("`___@_`___")),
|
||
Kem(ing("___`_`____"),ing("`_@___`___")),
|
||
)
|
||
|
||
init {
|
||
// automatically create mirrored version of the kerningRules
|
||
val imax = kerningRules.size // to avoid concurrentmodificationshit
|
||
for (i in 0 until imax) {
|
||
val left = kerningRules[i].first.s
|
||
val right = kerningRules[i].second.s
|
||
val bb = kerningRules[i].bb
|
||
val yy = kerningRules[i].yy
|
||
|
||
val newleft = StringBuilder()
|
||
val newright = StringBuilder()
|
||
|
||
if (left.length != right.length && left.length % 2 != 0) throw IllegalArgumentException()
|
||
|
||
for (c in 0 until left.length step 2) {
|
||
newleft.append(right[c+1],right[c])
|
||
newright.append(left[c+1],left[c])
|
||
}
|
||
|
||
kerningRules.add(Kem(ing("$newleft"),ing("$newright"),bb,yy))
|
||
}
|
||
|
||
|
||
// kerningRules.forEach { println("Keming ${it.first.s} - ${it.second.s} ; ${it.bb}/${it.yy}") }
|
||
}
|
||
|
||
// End of the Keming Machine
|
||
}
|
||
|
||
}
|