mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-15 23:46:09 +09:00
1808 lines
73 KiB
Kotlin
Executable File
1808 lines
73 KiB
Kotlin
Executable File
/*
|
||
* Terrarum Sans Bitmap
|
||
*
|
||
* Copyright (c) 2017-2020 Minjae Song (Torvald)
|
||
*
|
||
* 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.GlyphProps
|
||
import java.io.BufferedOutputStream
|
||
import java.io.FileOutputStream
|
||
import java.security.MessageDigest
|
||
import java.util.*
|
||
import java.util.zip.CRC32
|
||
import java.util.zip.GZIPInputStream
|
||
import kotlin.math.roundToInt
|
||
import kotlin.math.sign
|
||
|
||
typealias CodepointSequence = ArrayList<CodePoint>
|
||
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.gz
|
||
* - 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. For glyph getting upside-down, set ```TextureRegionPack.globalFlipY = true```.
|
||
*
|
||
* Created by minjaesong on 2017-06-15.
|
||
*/
|
||
class GameFontBase(
|
||
fontDir: String,
|
||
val noShadow: Boolean = false,
|
||
val flipY: Boolean = false,
|
||
val invertShadow: Boolean = false,
|
||
val minFilter: Texture.TextureFilter = Texture.TextureFilter.Nearest,
|
||
val magFilter: Texture.TextureFilter = Texture.TextureFilter.Nearest,
|
||
var errorOnUnknownChar: Boolean = false,
|
||
val textCacheSize: Int = 256,
|
||
val debug: Boolean = false
|
||
) : BitmapFont() {
|
||
|
||
/**
|
||
* lowercase AND the height is equal to x-height (e.g. lowercase B, D, F, H, K, L, ... does not count
|
||
*/
|
||
private fun CodePoint.isLowHeight() = this in lowHeightLetters
|
||
|
||
|
||
// TODO (val posXbuffer: IntArray, val posYbuffer: IntArray) -> (val linotype: Pixmap)
|
||
private data class ShittyGlyphLayout(val textBuffer: CodepointSequence, val linotype: Texture, val width: Int)
|
||
|
||
//private val textCache = HashMap<CharSequence, ShittyGlyphLayout>()
|
||
|
||
private data class TextCacheObj(var age: Int, val hash: Long, val glyphLayout: ShittyGlyphLayout?): Comparable<TextCacheObj> {
|
||
fun dispose() {
|
||
glyphLayout?.linotype?.dispose()
|
||
}
|
||
|
||
override fun compareTo(other: TextCacheObj): Int {
|
||
return (this.hash - other.hash).sign
|
||
}
|
||
}
|
||
private var textCacheCap = 0
|
||
|
||
private val textCache = Array(textCacheSize) { TextCacheObj(-1, -1L, null) }
|
||
|
||
/**
|
||
* Insertion sorts the last element fo the textCache
|
||
*/
|
||
private fun addToCache(text: CodepointSequence, linotype: Texture, width: Int) {
|
||
// Caching rules:
|
||
// 1. always accept new element.
|
||
// 2. often-called element have higher chance of survival (see: getCache(long))
|
||
|
||
|
||
if (textCacheCap < textCacheSize) {
|
||
textCache[textCacheCap] = TextCacheObj(0, text.getHash(), ShittyGlyphLayout(text, linotype, width))
|
||
textCacheCap += 1
|
||
|
||
// make everybody age
|
||
textCache.forEach {
|
||
it.age += 1
|
||
}
|
||
}
|
||
else {
|
||
// search for an oldest element
|
||
var oldestElemIndex = 0
|
||
var ageOfOldest = textCacheCap
|
||
textCache.forEachIndexed { index, it ->
|
||
// make everybody age
|
||
textCache[index].age += 1
|
||
|
||
// mark oldest
|
||
if (it.age > ageOfOldest) {
|
||
oldestElemIndex = index
|
||
}
|
||
}
|
||
|
||
|
||
// dispose of the oldest one before overwriting
|
||
textCache[oldestElemIndex].dispose()
|
||
|
||
// overwrite oldest one
|
||
textCache[oldestElemIndex] = TextCacheObj(0, text.getHash(), ShittyGlyphLayout(text, linotype, width))
|
||
}
|
||
|
||
// sort the list
|
||
textCache.sortBy { it.hash }
|
||
}
|
||
|
||
private fun getCache(hash: Long): TextCacheObj? {
|
||
var low = 0
|
||
var high = textCacheCap - 1
|
||
var key = -1
|
||
|
||
|
||
while (low <= high) {
|
||
val mid = (low + high).ushr(1) // safe from overflows
|
||
|
||
val midVal = textCache[mid]
|
||
|
||
if (hash > midVal.hash)
|
||
low = mid + 1
|
||
else if (hash < midVal.hash)
|
||
high = mid - 1
|
||
else {
|
||
key = mid
|
||
break // the condition is not enough to break the loop
|
||
}
|
||
}
|
||
|
||
if (key < 0)
|
||
return null
|
||
|
||
// decrement age count (see: addToCache(CodepointSequence, Pixmap, Int))
|
||
if (textCache[key].age > 0) textCache[key].age -= 1
|
||
|
||
return textCache[key]
|
||
}
|
||
|
||
|
||
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> = HashMap()
|
||
private val sheets: Array<PixmapRegionPack>
|
||
|
||
private var charsetOverride = 0
|
||
|
||
init {
|
||
val sheetsPack = ArrayList<PixmapRegionPack>()
|
||
|
||
// first we create pixmap to read pixels, then make texture using pixmap
|
||
fileList.forEachIndexed { index, it ->
|
||
val isVariable1 = it.endsWith("_variable.tga")
|
||
val isVariable2 = variableWidthSheets.contains(index)
|
||
val isVariable = isVariable1 && isVariable2
|
||
val isXYSwapped = it.contains("xyswap", true)
|
||
|
||
// idiocity check
|
||
if (isVariable1 && !isVariable2)
|
||
throw Error("[TerrarumSansBitmap] font is named as variable on the name but not enlisted as")
|
||
else if (!isVariable1 && isVariable2)
|
||
throw Error("[TerrarumSansBitmap] font is enlisted as variable on the name but not named as")
|
||
|
||
|
||
var pixmap: Pixmap
|
||
|
||
|
||
if (isVariable) {
|
||
if (isXYSwapped) {
|
||
println("[TerrarumSansBitmap] loading texture $it [VARIABLE, XYSWAP]")
|
||
}
|
||
else {
|
||
println("[TerrarumSansBitmap] loading texture $it [VARIABLE]")
|
||
}
|
||
}
|
||
else {
|
||
if (isXYSwapped) {
|
||
println("[TerrarumSansBitmap] loading texture $it [XYSWAP]")
|
||
}
|
||
else {
|
||
println("[TerrarumSansBitmap] loading texture $it")
|
||
}
|
||
}
|
||
|
||
|
||
// unpack gz if applicable
|
||
if (it.endsWith(".gz")) {
|
||
val tmpFileName = "tmp_${it.dropLast(7)}.tga"
|
||
|
||
try {
|
||
val gzi = GZIPInputStream(Gdx.files.internal(fontParentDir + it).read(8192))
|
||
val wholeFile = gzi.readBytes()
|
||
gzi.close()
|
||
val fos = BufferedOutputStream(FileOutputStream(tmpFileName))
|
||
fos.write(wholeFile)
|
||
fos.flush()
|
||
fos.close()
|
||
|
||
pixmap = Pixmap(Gdx.files.internal(tmpFileName))
|
||
}
|
||
catch (e: GdxRuntimeException) {
|
||
//e.printStackTrace()
|
||
System.err.println("[TerrarumSansBitmap] said texture not found, skipping...")
|
||
|
||
pixmap = Pixmap(1, 1, Pixmap.Format.RGBA8888)
|
||
}
|
||
//File(tmpFileName).delete()
|
||
}
|
||
else {
|
||
try {
|
||
pixmap = Pixmap(Gdx.files.internal(fontParentDir + it))
|
||
}
|
||
catch (e: GdxRuntimeException) {
|
||
//e.printStackTrace()
|
||
System.err.println("[TerrarumSansBitmap] 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) {
|
||
System.err.println("[TerrarumSansBitmap] The ASCII sheet is gone, something is wrong.")
|
||
System.exit(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isVariable) buildWidthTable(pixmap, codeRange[index], 16)
|
||
buildWidthTableFixed()
|
||
|
||
|
||
/*if (!noShadow) {
|
||
makeShadowForSheet(pixmap)
|
||
}*/
|
||
|
||
|
||
//val texture = Texture(pixmap)
|
||
val texRegPack = 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_FW_UNI) {
|
||
PixmapRegionPack(pixmap, W_UNIHAN, H)
|
||
}
|
||
else if (index == SHEET_CJK_PUNCT) {
|
||
PixmapRegionPack(pixmap, W_ASIAN_PUNCT, H)
|
||
}
|
||
else if (index == SHEET_KANA) {
|
||
PixmapRegionPack(pixmap, W_KANA, 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("[TerrarumSansBitmap] Unknown sheet index: $index")
|
||
|
||
//texRegPack.texture.setFilter(minFilter, magFilter)
|
||
|
||
sheetsPack.add(texRegPack)
|
||
|
||
pixmap.dispose() // you are terminated
|
||
}
|
||
|
||
sheets = sheetsPack.toTypedArray()
|
||
}
|
||
|
||
override fun getLineHeight(): Float = H.toFloat()
|
||
|
||
override fun getXHeight() = lineHeight
|
||
override fun getCapHeight() = lineHeight
|
||
override fun getAscent() = 0f
|
||
override fun getDescent() = 0f
|
||
|
||
override fun isFlipped() = false
|
||
|
||
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(256)
|
||
|
||
private lateinit var tempLinotype: Texture
|
||
|
||
private lateinit var originalColour: Color
|
||
|
||
private var nullProp = GlyphProps(15, 0)
|
||
|
||
private val pixmapOffsetY = 10
|
||
|
||
override fun draw(batch: Batch, charSeq: CharSequence, x: Float, y: Float): GlyphLayout? {
|
||
if (debug)
|
||
println("[TerrarumSansBitmap] max age: $textCacheCap")
|
||
|
||
fun Int.flipY() = this * if (flipY) 1 else -1
|
||
|
||
// always draw at integer position; this is bitmap font after all
|
||
val x = Math.round(x)
|
||
val y = Math.round(y)
|
||
|
||
originalColour = batch.color.cpy()
|
||
val mainColObj = originalColour
|
||
var mainCol: Int = originalColour.toRGBA8888().forceOpaque()
|
||
|
||
val charSeqHash = charSeq.toCodePoints().getHash()
|
||
|
||
if (charSeq.isNotBlank()) {
|
||
|
||
val cacheObj = getCache(charSeqHash)
|
||
|
||
if (cacheObj == null || flagFirstRun) {
|
||
textBuffer = charSeq.toCodePoints()
|
||
|
||
val (posXbuffer, posYbuffer) = buildWidthAndPosBuffers(textBuffer)
|
||
|
||
flagFirstRun = false
|
||
|
||
//println("text not in buffer: $charSeq")
|
||
|
||
|
||
//textBuffer.forEach { print("${it.toHex()} ") }
|
||
//println()
|
||
|
||
|
||
resetHash(charSeq, x.toFloat(), y.toFloat())
|
||
|
||
val linotypePixmap = Pixmap(posXbuffer.last(), H + (pixmapOffsetY * 2), Pixmap.Format.RGBA8888)
|
||
|
||
|
||
var index = 0
|
||
while (index <= textBuffer.lastIndex) {
|
||
val 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) {
|
||
mainCol = originalColour.toRGBA8888().forceOpaque()
|
||
}
|
||
else {
|
||
mainCol = getColour(c)
|
||
}
|
||
}
|
||
else if (isCharsetOverride(c)) {
|
||
charsetOverride = c - CHARSET_OVERRIDE_DEFAULT
|
||
}
|
||
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.setColor(mainCol)
|
||
linotypePixmap.drawPixmap(choTex, posXbuffer[index], pixmapOffsetY, mainCol)
|
||
linotypePixmap.drawPixmap(jungTex, posXbuffer[index], pixmapOffsetY, mainCol)
|
||
linotypePixmap.drawPixmap(jongTex, posXbuffer[index], pixmapOffsetY, mainCol)
|
||
|
||
//batch.color = mainCol
|
||
//batch.draw(choTex, x + posXbuffer[index].toFloat(), y)
|
||
//batch.draw(jungTex, x + posXbuffer[index].toFloat(), y)
|
||
//batch.draw(hangulSheet.get(indexJong, jongRow), x + posXbuffer[index].toFloat(), y)
|
||
|
||
|
||
index += hangulLength - 1
|
||
|
||
}
|
||
else {
|
||
try {
|
||
val posY = posYbuffer[index].flipY() +
|
||
if (sheetID == SHEET_UNIHAN) // evil exceptions
|
||
offsetUnihan
|
||
else if (sheetID == SHEET_CUSTOM_SYM)
|
||
offsetCustomSym
|
||
else 0
|
||
|
||
val posX = posXbuffer[index]
|
||
val texture = sheets[sheetID].get(sheetX, sheetY)
|
||
|
||
linotypePixmap.drawPixmap(texture, posX, posY + pixmapOffsetY, mainCol)
|
||
|
||
//batch.color = mainCol
|
||
//batch.draw(texture, posX, posY)
|
||
|
||
}
|
||
catch (noSuchGlyph: ArrayIndexOutOfBoundsException) {
|
||
//batch.color = mainCol
|
||
}
|
||
}
|
||
|
||
|
||
index++
|
||
}
|
||
|
||
|
||
makeShadow(linotypePixmap)
|
||
|
||
tempLinotype = Texture(linotypePixmap)
|
||
tempLinotype.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest)
|
||
|
||
// put things into cache
|
||
//textCache[charSeq] = ShittyGlyphLayout(textBuffer, linotype!!)
|
||
addToCache(textBuffer, tempLinotype, posXbuffer.last())
|
||
linotypePixmap.dispose()
|
||
}
|
||
else {
|
||
textBuffer = cacheObj.glyphLayout!!.textBuffer
|
||
tempLinotype = cacheObj.glyphLayout!!.linotype
|
||
}
|
||
|
||
|
||
|
||
batch.color = mainColObj
|
||
|
||
if (!flipY) {
|
||
batch.draw(tempLinotype, x.toFloat(), (y - pixmapOffsetY).toFloat())
|
||
}
|
||
else {
|
||
batch.draw(tempLinotype,
|
||
x.toFloat(),
|
||
(y - pixmapOffsetY + (tempLinotype.height)).toFloat(),
|
||
(tempLinotype.width.toFloat()),
|
||
-(tempLinotype.height.toFloat())
|
||
)
|
||
}
|
||
|
||
}
|
||
|
||
batch.color = originalColour
|
||
return null
|
||
}
|
||
|
||
private fun Int.charInfo() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}: ${Character.getName(this)}"
|
||
|
||
|
||
override fun dispose() {
|
||
super.dispose()
|
||
textCache.forEach { it.dispose() }
|
||
sheets.forEach { it.dispose() }
|
||
}
|
||
|
||
private fun getSheetType(c: CodePoint): Int {
|
||
if (charsetOverride == 1 && isBulgarian(c))
|
||
return SHEET_BULGARIAN_VARW
|
||
else if (charsetOverride == 2 && isBulgarian(c))
|
||
return SHEET_SERBIAN_VARW
|
||
else if (isHangul(c))
|
||
return SHEET_HANGUL
|
||
else if (isKana(c))
|
||
return SHEET_KANA
|
||
else if (isUniHan(c))
|
||
return SHEET_UNIHAN
|
||
else if (isAscii(c))
|
||
return SHEET_ASCII_VARW
|
||
else if (isExtA(c))
|
||
return SHEET_EXTA_VARW
|
||
else if (isExtB(c))
|
||
return SHEET_EXTB_VARW
|
||
else if (isCyrilic(c))
|
||
return SHEET_CYRILIC_VARW
|
||
else if (isUniPunct(c))
|
||
return SHEET_UNI_PUNCT_VARW
|
||
else if (isCJKPunct(c))
|
||
return SHEET_CJK_PUNCT
|
||
else if (isFullwidthUni(c))
|
||
return SHEET_FW_UNI
|
||
else if (isGreek(c))
|
||
return SHEET_GREEK_VARW
|
||
else if (isThai(c))
|
||
return SHEET_THAI_VARW
|
||
else if (isCustomSym(c))
|
||
return SHEET_CUSTOM_SYM
|
||
else if (isArmenian(c))
|
||
return SHEET_HAYEREN_VARW
|
||
else if (isKartvelian(c))
|
||
return SHEET_KARTULI_VARW
|
||
else if (isIPA(c))
|
||
return SHEET_IPA_VARW
|
||
else if (isRunic(c))
|
||
return SHEET_RUNIC
|
||
else if (isLatinExtAdd(c))
|
||
return SHEET_LATIN_EXT_ADD_VARW
|
||
else if (isCherokee(c))
|
||
return SHEET_TSALAGI_VARW
|
||
else if (isInsular(c))
|
||
return SHEET_INSUAR_VARW
|
||
else if (isNagariBengali(c))
|
||
return SHEET_NAGARI_BENGALI_VARW
|
||
else if (isKartvelianCaps(c))
|
||
return SHEET_KARTULI_CAPS_VARW
|
||
else if (isDiacriticalMarks(c))
|
||
return SHEET_DIACRITICAL_MARKS_VARW
|
||
else if (isPolytonicGreek(c))
|
||
return SHEET_GREEK_POLY_VARW
|
||
else if (isExtC(c))
|
||
return SHEET_EXTC_VARW
|
||
else if (isExtD(c))
|
||
return SHEET_EXTD_VARW
|
||
else
|
||
return SHEET_UNKNOWN
|
||
// fixed width
|
||
// fallback
|
||
}
|
||
|
||
private fun getSheetwisePosition(cPrev: Int, ch: Int): IntArray {
|
||
val sheetX: Int; val sheetY: Int
|
||
when (getSheetType(ch)) {
|
||
SHEET_UNIHAN -> {
|
||
sheetX = unihanIndexX(ch)
|
||
sheetY = unihanIndexY(ch)
|
||
}
|
||
SHEET_EXTA_VARW -> {
|
||
sheetX = extAindexX(ch)
|
||
sheetY = extAindexY(ch)
|
||
}
|
||
SHEET_EXTB_VARW -> {
|
||
sheetX = extBindexX(ch)
|
||
sheetY = extBindexY(ch)
|
||
}
|
||
SHEET_KANA -> {
|
||
sheetX = kanaIndexX(ch)
|
||
sheetY = kanaIndexY(ch)
|
||
}
|
||
SHEET_CJK_PUNCT -> {
|
||
sheetX = cjkPunctIndexX(ch)
|
||
sheetY = cjkPunctIndexY(ch)
|
||
}
|
||
SHEET_CYRILIC_VARW -> {
|
||
sheetX = cyrilicIndexX(ch)
|
||
sheetY = cyrilicIndexY(ch)
|
||
}
|
||
SHEET_FW_UNI -> {
|
||
sheetX = fullwidthUniIndexX(ch)
|
||
sheetY = fullwidthUniIndexY(ch)
|
||
}
|
||
SHEET_UNI_PUNCT_VARW -> {
|
||
sheetX = uniPunctIndexX(ch)
|
||
sheetY = uniPunctIndexY(ch)
|
||
}
|
||
SHEET_GREEK_VARW -> {
|
||
sheetX = greekIndexX(ch)
|
||
sheetY = greekIndexY(ch)
|
||
}
|
||
SHEET_THAI_VARW -> {
|
||
sheetX = thaiIndexX(ch)
|
||
sheetY = thaiIndexY(ch)
|
||
}
|
||
SHEET_CUSTOM_SYM -> {
|
||
sheetX = symbolIndexX(ch)
|
||
sheetY = symbolIndexY(ch)
|
||
}
|
||
SHEET_HAYEREN_VARW -> {
|
||
sheetX = armenianIndexX(ch)
|
||
sheetY = armenianIndexY(ch)
|
||
}
|
||
SHEET_KARTULI_VARW -> {
|
||
sheetX = kartvelianIndexX(ch)
|
||
sheetY = kartvelianIndexY(ch)
|
||
}
|
||
SHEET_IPA_VARW -> {
|
||
sheetX = ipaIndexX(ch)
|
||
sheetY = ipaIndexY(ch)
|
||
}
|
||
SHEET_RUNIC -> {
|
||
sheetX = runicIndexX(ch)
|
||
sheetY = runicIndexY(ch)
|
||
}
|
||
SHEET_LATIN_EXT_ADD_VARW -> {
|
||
sheetX = latinExtAddX(ch)
|
||
sheetY = latinExtAddY(ch)
|
||
}
|
||
SHEET_BULGARIAN_VARW, SHEET_SERBIAN_VARW -> { // expects Unicode charpoint, NOT an internal one
|
||
sheetX = cyrilicIndexX(ch)
|
||
sheetY = cyrilicIndexY(ch)
|
||
}
|
||
SHEET_TSALAGI_VARW -> {
|
||
sheetX = cherokeeIndexX(ch)
|
||
sheetY = cherokeeIndexY(ch)
|
||
}
|
||
SHEET_INSUAR_VARW -> {
|
||
sheetX = insularIndexX(ch)
|
||
sheetY = insularIndexY(ch)
|
||
}
|
||
SHEET_NAGARI_BENGALI_VARW -> {
|
||
sheetX = nagariIndexX(ch)
|
||
sheetY = nagariIndexY(ch)
|
||
}
|
||
SHEET_KARTULI_CAPS_VARW -> {
|
||
sheetX = kartvelianCapsIndexX(ch)
|
||
sheetY = kartvelianCapsIndexY(ch)
|
||
}
|
||
SHEET_DIACRITICAL_MARKS_VARW -> {
|
||
sheetX = diacriticalMarksIndexX(ch)
|
||
sheetY = diacriticalMarksIndexY(ch)
|
||
}
|
||
SHEET_GREEK_POLY_VARW -> {
|
||
sheetX = polytonicGreekIndexX(ch)
|
||
sheetY = polytonicGreekIndexY(ch)
|
||
}
|
||
SHEET_EXTC_VARW -> {
|
||
sheetX = extCIndexX(ch)
|
||
sheetY = extCIndexY(ch)
|
||
}
|
||
SHEET_EXTD_VARW -> {
|
||
sheetX = extDIndexX(ch)
|
||
sheetY = extDIndexY(ch)
|
||
}
|
||
else -> {
|
||
sheetX = ch % 16
|
||
sheetY = ch / 16
|
||
}
|
||
}
|
||
|
||
return intArrayOf(sheetX, sheetY)
|
||
}
|
||
|
||
fun buildWidthTable(pixmap: Pixmap, codeRange: Iterable<Int>, cols: Int = 16) {
|
||
val binaryCodeOffset = W_VAR_INIT
|
||
|
||
val cellW = W_VAR_INIT + 1
|
||
val cellH = H
|
||
|
||
for (code in codeRange) {
|
||
|
||
val cellX = ((code - codeRange.first()) % cols) * cellW
|
||
val cellY = ((code - codeRange.first()) / cols) * cellH
|
||
|
||
val codeStartX = cellX + binaryCodeOffset
|
||
val codeStartY = cellY
|
||
val tagStartY = codeStartY + 10
|
||
|
||
var width = 0
|
||
var tags = 0
|
||
|
||
for (y in 0..3) {
|
||
// if ALPHA is not zero, assume it's 1
|
||
if (pixmap.getPixel(codeStartX, codeStartY + y).and(0xFF) != 0) {
|
||
width = width or (1 shl y)
|
||
}
|
||
}
|
||
|
||
for (y in 0..9) {
|
||
// if ALPHA is not zero, assume it's 1
|
||
if (pixmap.getPixel(codeStartX, tagStartY + y).and(0xFF) != 0) {
|
||
tags = tags or (1 shl y)
|
||
}
|
||
}
|
||
|
||
//println("$code: Width $width, tags $tags")
|
||
|
||
/*val isDiacritics = pixmap.getPixel(codeStartX, codeStartY + H - 1).and(0xFF) != 0
|
||
if (isDiacritics)
|
||
glyphWidth = -glyphWidth*/
|
||
|
||
glyphProps[code] = GlyphProps(width, tags)
|
||
|
||
// extra info
|
||
val extCount = glyphProps[code]?.requiredExtInfoCount() ?: 0
|
||
if (extCount > 0) {
|
||
|
||
glyphProps[code]?.extInfo = IntArray(extCount)
|
||
|
||
for (x in 0 until extCount) {
|
||
var info = 0
|
||
for (y in 0..18) {
|
||
// if ALPHA is not zero, assume it's 1
|
||
if (pixmap.getPixel(cellX + x, cellY + y).and(0xFF) != 0) {
|
||
info = info or (1 shl y)
|
||
}
|
||
}
|
||
|
||
glyphProps[code]!!.extInfo!![x] = info
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun buildWidthTableFixed() {
|
||
// fixed-width props
|
||
codeRange[SHEET_CJK_PUNCT].forEach { glyphProps[it] = GlyphProps(W_ASIAN_PUNCT, 0) }
|
||
codeRange[SHEET_CUSTOM_SYM].forEach { glyphProps[it] = GlyphProps(20, 0) }
|
||
codeRange[SHEET_FW_UNI].forEach { glyphProps[it] = GlyphProps(W_UNIHAN, 0) }
|
||
codeRange[SHEET_HANGUL].forEach { glyphProps[it] = GlyphProps(W_HANGUL_BASE, 0) }
|
||
codeRangeHangulCompat.forEach { glyphProps[it] = GlyphProps(W_HANGUL_BASE, 0) }
|
||
codeRange[SHEET_KANA].forEach { glyphProps[it] = GlyphProps(W_KANA, 0) }
|
||
codeRange[SHEET_RUNIC].forEach { glyphProps[it] = GlyphProps(9, 0) }
|
||
codeRange[SHEET_UNIHAN].forEach { glyphProps[it] = GlyphProps(W_UNIHAN, 0) }
|
||
(0xD800..0xDFFF).forEach { glyphProps[it] = GlyphProps(0, 0) }
|
||
(0x100000..0x10FFFF).forEach { glyphProps[it] = GlyphProps(0, 0) }
|
||
(0xFFFA0..0xFFFFF).forEach { glyphProps[it] = GlyphProps(0, 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, 0)
|
||
|
||
|
||
// 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, 0)
|
||
|
||
}
|
||
|
||
private val glyphLayout = GlyphLayout()
|
||
|
||
fun getWidth(text: String) = getWidth(text.toCodePoints())
|
||
|
||
fun getWidth(s: CodepointSequence): Int {
|
||
val cacheObj = getCache(s.getHash())
|
||
|
||
if (cacheObj != null) {
|
||
return cacheObj.glyphLayout!!.width
|
||
}
|
||
else {
|
||
return buildWidthAndPosBuffers(s).first.last()
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* posXbuffer's size is greater than the string, last element marks the width of entire string.
|
||
*/
|
||
private fun buildWidthAndPosBuffers(str: CodepointSequence): Pair<IntArray, IntArray> {
|
||
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
|
||
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
|
||
|
||
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)
|
||
|
||
|
||
//println("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
|
||
)) {
|
||
//println("char: ${thisChar.charInfo()}\nproperties: $thisProp")
|
||
//println("${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]
|
||
}
|
||
else if (!thisProp.writeOnTop) {
|
||
posXbuffer[charIndex] = when (itsProp.alignWhere) {
|
||
GlyphProps.ALIGN_RIGHT ->
|
||
posXbuffer[nonDiacriticCounter] + W_VAR_INIT + alignmentOffset + interchar + kerning + extraWidth
|
||
GlyphProps.ALIGN_CENTRE ->
|
||
posXbuffer[nonDiacriticCounter] + HALF_VAR_INIT + itsProp.width + alignmentOffset + interchar + kerning + extraWidth
|
||
else ->
|
||
posXbuffer[nonDiacriticCounter] + itsProp.width + alignmentOffset + interchar + kerning + extraWidth
|
||
}
|
||
|
||
nonDiacriticCounter = charIndex
|
||
|
||
stackUpwardCounter = 0
|
||
stackDownwardCounter = 0
|
||
extraWidth = 0
|
||
}
|
||
else if (thisProp.writeOnTop && thisProp.alignXPos == 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
|
||
|
||
}
|
||
}
|
||
else {
|
||
// set X pos according to alignment information
|
||
posXbuffer[charIndex] = when (thisProp.alignWhere) {
|
||
GlyphProps.ALIGN_LEFT, GlyphProps.ALIGN_BEFORE -> posXbuffer[nonDiacriticCounter]
|
||
GlyphProps.ALIGN_RIGHT -> {
|
||
posXbuffer[nonDiacriticCounter] - (W_VAR_INIT - itsProp.width)
|
||
}
|
||
GlyphProps.ALIGN_CENTRE -> {
|
||
val alignXPos = if (itsProp.alignXPos == 0) itsProp.width.div(2) else itsProp.alignXPos
|
||
|
||
if (itsProp.alignWhere == GlyphProps.ALIGN_RIGHT) {
|
||
posXbuffer[nonDiacriticCounter] + alignXPos + (itsProp.width + 1).div(2)
|
||
}
|
||
else {
|
||
posXbuffer[nonDiacriticCounter] + alignXPos - HALF_VAR_INIT
|
||
}
|
||
}
|
||
else -> throw InternalError("Unsupported alignment: ${thisProp.alignWhere}")
|
||
}
|
||
|
||
|
||
// set Y pos according to diacritics position
|
||
if (thisProp.alignWhere == GlyphProps.ALIGN_CENTRE) {
|
||
when (thisProp.stackWhere) {
|
||
GlyphProps.STACK_DOWN -> {
|
||
posYbuffer[charIndex] = H_DIACRITICS * stackDownwardCounter
|
||
stackDownwardCounter++
|
||
}
|
||
GlyphProps.STACK_UP -> {
|
||
posYbuffer[charIndex] = -H_DIACRITICS * stackUpwardCounter
|
||
|
||
// shift down on lowercase if applicable
|
||
if (getSheetType(thisChar) in autoShiftDownOnLowercase &&
|
||
lastNonDiacriticChar.isLowHeight()) {
|
||
if (thisProp.alignXPos == GlyphProps.DIA_OVERLAY)
|
||
posYbuffer[charIndex] += H_OVERLAY_LOWERCASE_SHIFTDOWN
|
||
else
|
||
posYbuffer[charIndex] += H_STACKUP_LOWERCASE_SHIFTDOWN
|
||
}
|
||
|
||
stackUpwardCounter++
|
||
}
|
||
GlyphProps.STACK_UP_N_DOWN -> {
|
||
posYbuffer[charIndex] = H_DIACRITICS * stackDownwardCounter
|
||
stackDownwardCounter++
|
||
posYbuffer[charIndex] = -H_DIACRITICS * stackUpwardCounter
|
||
stackUpwardCounter++
|
||
}
|
||
// for BEFORE_N_AFTER, do nothing in here
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// fill the last of the posXbuffer
|
||
if (str.isNotEmpty()) {
|
||
val lastCharProp = glyphProps[str.last()]
|
||
val penultCharProp = glyphProps[nonDiacriticCounter]!!
|
||
posXbuffer[posXbuffer.lastIndex] = 1 + posXbuffer[posXbuffer.lastIndex - 1] + // adding 1 to house the shadow
|
||
if (lastCharProp?.writeOnTop == true) {
|
||
val realDiacriticWidth = if (lastCharProp.alignWhere == GlyphProps.ALIGN_CENTRE) {
|
||
(lastCharProp.width).div(2) + penultCharProp.alignXPos
|
||
}
|
||
else if (lastCharProp.alignWhere == GlyphProps.ALIGN_RIGHT) {
|
||
(lastCharProp.width) + penultCharProp.alignXPos
|
||
}
|
||
else 0
|
||
|
||
maxOf(penultCharProp.width, realDiacriticWidth)
|
||
}
|
||
else {
|
||
(lastCharProp?.width ?: 0)
|
||
}
|
||
}
|
||
else {
|
||
posXbuffer[0] = 0
|
||
}
|
||
|
||
return posXbuffer to posYbuffer
|
||
}
|
||
|
||
|
||
/** 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 !!
|
||
*/
|
||
private fun CharSequence.toCodePoints(): CodepointSequence {
|
||
val seq = ArrayList<Int>()
|
||
|
||
var i = 0
|
||
while (i < this.length) {
|
||
val c = this[i]
|
||
|
||
// LET THE NORMALISATION BEGIN //
|
||
|
||
// 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(Character.toCodePoint(H, L))
|
||
|
||
i++ // skip next char (guaranteed to be Low Surrogate)
|
||
}
|
||
}
|
||
// disassemble Hangul Syllables into Initial-Peak-Final encoding
|
||
else if (c in 0xAC00.toChar()..0xD7A3.toChar()) {
|
||
val cInt = c.toInt() - 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.toChar()..0x33FF.toChar()) {
|
||
seq.add(0x7F) // fuck them
|
||
}
|
||
// rearrange {letter, before-and-after diacritics} as {letter, before-diacritics, after-diacritics}
|
||
// {letter, before-diacritics} part will be dealt with swapping code below
|
||
// DOES NOT WORK if said diacritics has codepoint > 0xFFFF
|
||
else if (i < this.lastIndex && this[i + 1].toInt() <= 0xFFFF &&
|
||
glyphProps[this[i + 1].toInt()]?.stackWhere == GlyphProps.STACK_BEFORE_N_AFTER) {
|
||
val diacriticsProp = glyphProps[this[i + 1].toInt()]!!
|
||
seq.add(c.toInt())
|
||
seq.add(diacriticsProp.extInfo!![0])
|
||
seq.add(diacriticsProp.extInfo!![1])
|
||
i++
|
||
}
|
||
// U+007F is DEL originally, but this font stores bitmap of Replacement Character (U+FFFD)
|
||
// to this position. This line will replace U+FFFD into U+007F.
|
||
else if (c == 0xFFFD.toChar()) {
|
||
seq.add(0x7F) // 0x7F in used internally to display <??> character
|
||
}
|
||
else {
|
||
seq.add(c.toInt())
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
// swap position of {letter, diacritics that comes before the letter}
|
||
i = 1
|
||
while (i <= seq.lastIndex) {
|
||
|
||
if ((glyphProps[seq[i]] ?: nullProp).alignWhere == GlyphProps.ALIGN_BEFORE) {
|
||
val t = seq[i - 1]
|
||
seq[i - 1] = seq[i]
|
||
seq[i] = t
|
||
}
|
||
|
||
i++
|
||
}
|
||
|
||
return seq
|
||
}
|
||
|
||
|
||
/**
|
||
* 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 0 until pixmap.height) {
|
||
for (x in 0 until pixmap.width) {
|
||
val pixel = pixmap.getPixel(x, y) // RGBA8888
|
||
|
||
|
||
// in the current version, all colour-coded glyphs are guaranteed
|
||
// to be opaque
|
||
if (pixel and 0xFF == 0xFF) {
|
||
val newPixel = pixmap.getPixel(x + it.first, y + it.second)
|
||
val newColour = pixel.and(0xFFFFFF00.toInt()) or 0x80
|
||
|
||
if (newPixel and 0xFF == 0) {
|
||
pixmap.drawPixel(x + it.first, y + it.second, newColour)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
/***
|
||
* @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)
|
||
/** CodePoint = 0x10000 + (H - 0xD800) * 0x400 + (L - 0xDC00) */
|
||
private fun Char.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 = GameFontBase.toColorCode(argb4444)
|
||
fun toColorCode(r: Int, g: Int, b: Int, a: Int = 0x0F): String = GameFontBase.toColorCode(r, g, b, a)
|
||
val noColorCode = toColorCode(0x0000)
|
||
|
||
val charsetOverrideDefault = Character.toChars(CHARSET_OVERRIDE_DEFAULT)
|
||
val charsetOverrideBulgarian = Character.toChars(CHARSET_OVERRIDE_BG_BG)
|
||
val charsetOverrideSerbian = Character.toChars(CHARSET_OVERRIDE_SR_SR)
|
||
|
||
// 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())
|
||
}
|
||
|
||
|
||
fun CodepointSequence.getHash(): Long {
|
||
val hashBasis = -3750763034362895579L
|
||
val hashPrime = 1099511628211L
|
||
var hashAccumulator = hashBasis
|
||
|
||
this.forEach {
|
||
hashAccumulator = hashAccumulator xor it.toLong()
|
||
hashAccumulator *= hashPrime
|
||
}
|
||
|
||
return hashAccumulator
|
||
}
|
||
|
||
|
||
|
||
private fun CharSequence.sha256(): ByteArray {
|
||
val digest = MessageDigest.getInstance("SHA-256")
|
||
this.forEach {
|
||
val it = it.toInt()
|
||
val b1 = it.shl(8).and(255).toByte()
|
||
val b2 = it.and(255).toByte()
|
||
digest.update(b1)
|
||
digest.update(b2)
|
||
}
|
||
|
||
return digest.digest()
|
||
}
|
||
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()
|
||
}
|
||
|
||
companion object {
|
||
internal val JUNG_COUNT = 21
|
||
internal val JONG_COUNT = 28
|
||
|
||
internal val W_ASIAN_PUNCT = 10
|
||
internal val W_HANGUL_BASE = 13
|
||
internal val W_KANA = 12
|
||
internal val W_UNIHAN = 16
|
||
internal val W_LATIN_WIDE = 9 // width of regular letters
|
||
internal val W_VAR_INIT = 15 // it assumes width of 15 regardless of the tagged width
|
||
|
||
internal val HGAP_VAR = 1
|
||
|
||
internal val H = 20
|
||
internal val H_UNIHAN = 16
|
||
|
||
internal val H_DIACRITICS = 3
|
||
|
||
internal val H_STACKUP_LOWERCASE_SHIFTDOWN = 4
|
||
internal val H_OVERLAY_LOWERCASE_SHIFTDOWN = 2
|
||
|
||
internal val SIZE_CUSTOM_SYM = 18
|
||
|
||
internal val SHEET_ASCII_VARW = 0
|
||
internal val SHEET_HANGUL = 1
|
||
internal val SHEET_EXTA_VARW = 2
|
||
internal val SHEET_EXTB_VARW = 3
|
||
internal val SHEET_KANA = 4
|
||
internal val SHEET_CJK_PUNCT = 5
|
||
internal val SHEET_UNIHAN = 6
|
||
internal val SHEET_CYRILIC_VARW = 7
|
||
internal val SHEET_FW_UNI = 8
|
||
internal val SHEET_UNI_PUNCT_VARW = 9
|
||
internal val SHEET_GREEK_VARW = 10
|
||
internal val SHEET_THAI_VARW = 11
|
||
internal val SHEET_HAYEREN_VARW = 12
|
||
internal val SHEET_KARTULI_VARW = 13
|
||
internal val SHEET_IPA_VARW = 14
|
||
internal val SHEET_RUNIC = 15
|
||
internal val SHEET_LATIN_EXT_ADD_VARW= 16
|
||
internal val SHEET_CUSTOM_SYM = 17
|
||
internal val SHEET_BULGARIAN_VARW = 18
|
||
internal val SHEET_SERBIAN_VARW = 19
|
||
internal val SHEET_TSALAGI_VARW = 20
|
||
internal val SHEET_INSUAR_VARW = 21 // currently only for U+1D79
|
||
internal val SHEET_NAGARI_BENGALI_VARW=22
|
||
internal val SHEET_KARTULI_CAPS_VARW = 23
|
||
internal val SHEET_DIACRITICAL_MARKS_VARW = 24
|
||
internal val SHEET_GREEK_POLY_VARW = 25
|
||
internal val SHEET_EXTC_VARW = 26
|
||
internal val SHEET_EXTD_VARW = 27
|
||
|
||
internal val SHEET_UNKNOWN = 254
|
||
|
||
// custom codepoints
|
||
|
||
internal val RICH_TEXT_MODIFIER_RUBY_MASTER = 0xFFFA0
|
||
internal val RICH_TEXT_MODIFIER_RUBY_SLAVE = 0xFFFA1
|
||
internal val RICH_TEXT_MODIFIER_SUPERSCRIPT = 0xFFFA2
|
||
internal val RICH_TEXT_MODIFIER_SUBSCRIPT = 0xFFFA3
|
||
internal val RICH_TEXT_MODIFIER_TAG_END = 0xFFFBF
|
||
|
||
internal val CHARSET_OVERRIDE_DEFAULT = 0xFFFC0
|
||
internal val CHARSET_OVERRIDE_BG_BG = 0xFFFC1
|
||
internal val CHARSET_OVERRIDE_SR_SR = 0xFFFC2
|
||
|
||
|
||
private val unihanWidthSheets = arrayOf(
|
||
SHEET_UNIHAN,
|
||
SHEET_FW_UNI
|
||
)
|
||
private val variableWidthSheets = arrayOf(
|
||
SHEET_ASCII_VARW,
|
||
SHEET_EXTA_VARW,
|
||
SHEET_EXTB_VARW,
|
||
SHEET_CYRILIC_VARW,
|
||
SHEET_UNI_PUNCT_VARW,
|
||
SHEET_GREEK_VARW,
|
||
SHEET_THAI_VARW,
|
||
SHEET_HAYEREN_VARW,
|
||
SHEET_KARTULI_VARW,
|
||
SHEET_IPA_VARW,
|
||
SHEET_LATIN_EXT_ADD_VARW,
|
||
SHEET_BULGARIAN_VARW,
|
||
SHEET_SERBIAN_VARW,
|
||
SHEET_TSALAGI_VARW,
|
||
SHEET_INSUAR_VARW,
|
||
SHEET_NAGARI_BENGALI_VARW,
|
||
SHEET_KARTULI_CAPS_VARW,
|
||
SHEET_DIACRITICAL_MARKS_VARW,
|
||
SHEET_GREEK_POLY_VARW,
|
||
SHEET_EXTC_VARW,
|
||
SHEET_EXTD_VARW
|
||
)
|
||
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.tga",
|
||
"cjkpunct.tga",
|
||
"wenquanyi.tga.gz",
|
||
"cyrilic_variable.tga",
|
||
"fullwidth_forms.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",
|
||
"insular_variable.tga",
|
||
"devanagari_bengali_variable.tga",
|
||
"kartuli_allcaps_variable.tga",
|
||
"diacritical_marks_variable.tga",
|
||
"greek_polytonic_xyswap_variable.tga",
|
||
"latinExtC_variable.tga",
|
||
"latinExtD_variable.tga"
|
||
)
|
||
private 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) + (0x1B000..0x1B001), // SHEET_KANA
|
||
0x3000..0x303F, // SHEET_CJK_PUNCT
|
||
0x3400..0x9FFF, // SHEET_UNIHAN
|
||
0x400..0x52F, // SHEET_CYRILIC_VARW
|
||
0xFF00..0xFF1F, // SHEET_FW_UNI
|
||
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
|
||
0xF00000..0xF0005F, // SHEET_BULGARIAN_VARW; assign them to PUA
|
||
0xF00060..0xF000BF, // SHEET_SERBIAN_VARW; assign them to PUA
|
||
0x13A0..0x13F5, // SHEET_TSALAGI_VARW
|
||
0xA770..0xA787, // SHEET_INSULAR_VARW; if it work, don't fix it (yet--wait until Latin Extended C)
|
||
0x900..0x9FF, // SHEET_NAGARI_BENGALI_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
|
||
)
|
||
private val codeRangeHangulCompat = 0x3130..0x318F
|
||
|
||
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,80,83).toSortedSet()
|
||
// ㅘ ㅙ ㅞ
|
||
private val jungseongOUComplex = (arrayOf(10,11,16) + (22..33).toList() + arrayOf(36,37,38) + (41..44).toList() + arrayOf(46,47,48,49,50) + (56..59).toList() + arrayOf(63) + (67..79).toList() + arrayOf(81,82) + (84..93).toList()).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 jungseongWide = (jungseongOU.toList() + jungseongEU.toList()).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()
|
||
|
||
/**
|
||
* @param i Initial (Choseong)
|
||
* @param p Peak (Jungseong)
|
||
* @param f Final (Jongseong)
|
||
*/
|
||
private fun getHanInitialRow(i: Int, p: Int, f: Int): Int {
|
||
val ret =
|
||
if (p in jungseongI) 3
|
||
else if (p in jungseongOUComplex) 7
|
||
else if (p in jungseongOEWI) 11
|
||
else if (p in jungseongOU) 5
|
||
else if (p in jungseongEU) 9
|
||
else if (p in jungseongYI) 13
|
||
else 1
|
||
|
||
return if (f == 0) ret else ret + 1
|
||
}
|
||
|
||
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 //
|
||
|
||
private fun isHangul(c: CodePoint) = c in codeRange[SHEET_HANGUL] || c in 0x3130..0x318F
|
||
private fun isAscii(c: CodePoint) = c in codeRange[SHEET_ASCII_VARW]
|
||
private fun isRunic(c: CodePoint) = c in codeRange[SHEET_RUNIC]
|
||
private fun isExtA(c: CodePoint) = c in codeRange[SHEET_EXTA_VARW]
|
||
private fun isExtB(c: CodePoint) = c in codeRange[SHEET_EXTB_VARW]
|
||
private fun isKana(c: CodePoint) = c in codeRange[SHEET_KANA]
|
||
private fun isCJKPunct(c: CodePoint) = c in codeRange[SHEET_CJK_PUNCT]
|
||
private fun isUniHan(c: CodePoint) = c in codeRange[SHEET_UNIHAN]
|
||
private fun isCyrilic(c: CodePoint) = c in codeRange[SHEET_CYRILIC_VARW]
|
||
private fun isFullwidthUni(c: CodePoint) = c in codeRange[SHEET_FW_UNI]
|
||
private fun isUniPunct(c: CodePoint) = c in codeRange[SHEET_UNI_PUNCT_VARW]
|
||
private fun isGreek(c: CodePoint) = c in codeRange[SHEET_GREEK_VARW]
|
||
private fun isThai(c: CodePoint) = c in codeRange[SHEET_THAI_VARW]
|
||
/*private fun isDiacritics(c: CodePoint) = c in 0xE34..0xE3A
|
||
|| c in 0xE47..0xE4E
|
||
|| c == 0xE31*/
|
||
private fun isCustomSym(c: CodePoint) = c in codeRange[SHEET_CUSTOM_SYM]
|
||
private fun isArmenian(c: CodePoint) = c in codeRange[SHEET_HAYEREN_VARW]
|
||
private fun isKartvelian(c: CodePoint) = c in codeRange[SHEET_KARTULI_VARW]
|
||
private fun isIPA(c: CodePoint) = c in codeRange[SHEET_IPA_VARW]
|
||
private fun isLatinExtAdd(c: CodePoint) = c in 0x1E00..0x1EFF
|
||
private fun isBulgarian(c: CodePoint) = c in 0x400..0x45F
|
||
private fun isColourCode(c: CodePoint) = c == 0x100000 || c in 0x10F000..0x10FFFF
|
||
private fun isCharsetOverride(c: CodePoint) = c in 0xFFFC0..0xFFFFF
|
||
private fun isCherokee(c: CodePoint) = c in codeRange[SHEET_TSALAGI_VARW]
|
||
private fun isInsular(c: CodePoint) = c == 0x1D79
|
||
private fun isNagariBengali(c: CodePoint) = c in codeRange[SHEET_NAGARI_BENGALI_VARW]
|
||
private fun isKartvelianCaps(c: CodePoint) = c in codeRange[SHEET_KARTULI_CAPS_VARW]
|
||
private fun isDiacriticalMarks(c: CodePoint) = c in codeRange[SHEET_DIACRITICAL_MARKS_VARW]
|
||
private fun isPolytonicGreek(c: CodePoint) = c in codeRange[SHEET_GREEK_POLY_VARW]
|
||
private fun isExtC(c: CodePoint) = c in codeRange[SHEET_EXTC_VARW]
|
||
private fun isExtD(c: CodePoint) = c in codeRange[SHEET_EXTD_VARW]
|
||
private fun isHangulCompat(c: CodePoint) = c in codeRangeHangulCompat
|
||
|
||
// underscored name: not a charset
|
||
private fun _isCaps(c: CodePoint) = Character.isUpperCase(c) || isKartvelianCaps(c)
|
||
|
||
|
||
private fun extAindexX(c: CodePoint) = (c - 0x100) % 16
|
||
private fun extAindexY(c: CodePoint) = (c - 0x100) / 16
|
||
|
||
private fun extBindexX(c: CodePoint) = (c - 0x180) % 16
|
||
private fun extBindexY(c: CodePoint) = (c - 0x180) / 16
|
||
|
||
private fun runicIndexX(c: CodePoint) = (c - 0x16A0) % 16
|
||
private fun runicIndexY(c: CodePoint) = (c - 0x16A0) / 16
|
||
|
||
private fun kanaIndexX(c: CodePoint) = (c - 0x3040) % 16
|
||
private fun kanaIndexY(c: CodePoint) =
|
||
if (c in 0x31F0..0x31FF) 12
|
||
else if (c in 0x1B000..0x1B00F) 13
|
||
else (c - 0x3040) / 16
|
||
|
||
private fun cjkPunctIndexX(c: CodePoint) = (c - 0x3000) % 16
|
||
private fun cjkPunctIndexY(c: CodePoint) = (c - 0x3000) / 16
|
||
|
||
private fun cyrilicIndexX(c: CodePoint) = (c - 0x400) % 16
|
||
private fun cyrilicIndexY(c: CodePoint) = (c - 0x400) / 16
|
||
|
||
private fun fullwidthUniIndexX(c: CodePoint) = (c - 0xFF00) % 16
|
||
private fun fullwidthUniIndexY(c: CodePoint) = (c - 0xFF00) / 16
|
||
|
||
private fun uniPunctIndexX(c: CodePoint) = (c - 0x2000) % 16
|
||
private fun uniPunctIndexY(c: CodePoint) = (c - 0x2000) / 16
|
||
|
||
private fun unihanIndexX(c: CodePoint) = (c - 0x3400) % 256
|
||
private fun unihanIndexY(c: CodePoint) = (c - 0x3400) / 256
|
||
|
||
private fun greekIndexX(c: CodePoint) = (c - 0x370) % 16
|
||
private fun greekIndexY(c: CodePoint) = (c - 0x370) / 16
|
||
|
||
private fun thaiIndexX(c: CodePoint) = (c - 0xE00) % 16
|
||
private fun thaiIndexY(c: CodePoint) = (c - 0xE00) / 16
|
||
|
||
private fun symbolIndexX(c: CodePoint) = (c - 0xE000) % 16
|
||
private fun symbolIndexY(c: CodePoint) = (c - 0xE000) / 16
|
||
|
||
private fun armenianIndexX(c: CodePoint) = (c - 0x530) % 16
|
||
private fun armenianIndexY(c: CodePoint) = (c - 0x530) / 16
|
||
|
||
private fun kartvelianIndexX(c: CodePoint) = (c - 0x10D0) % 16
|
||
private fun kartvelianIndexY(c: CodePoint) = (c - 0x10D0) / 16
|
||
|
||
private fun ipaIndexX(c: CodePoint) = (c - 0x250) % 16
|
||
private fun ipaIndexY(c: CodePoint) = (c - 0x250) / 16
|
||
|
||
private fun latinExtAddX(c: CodePoint) = (c - 0x1E00) % 16
|
||
private fun latinExtAddY(c: CodePoint) = (c - 0x1E00) / 16
|
||
|
||
private fun cherokeeIndexX(c: CodePoint) = (c - 0x13A0) % 16
|
||
private fun cherokeeIndexY(c: CodePoint) = (c - 0x13A0) / 16
|
||
|
||
private fun insularIndexX(c: CodePoint) =
|
||
if (c == 0x1D79) 0 else (c - 0xA770) % 16
|
||
private fun insularIndexY(c: CodePoint) =
|
||
if (c == 0x1D79) 0 else (c - 0xA770) / 16
|
||
|
||
private fun nagariIndexX(c: CodePoint) = (c - 0x900) % 16
|
||
private fun nagariIndexY(c: CodePoint) = (c - 0x900) / 16
|
||
|
||
private fun kartvelianCapsIndexX(c: CodePoint) = (c - 0x1C90) % 16
|
||
private fun kartvelianCapsIndexY(c: CodePoint) = (c - 0x1C90) / 16
|
||
|
||
private fun diacriticalMarksIndexX(c: CodePoint) = (c - 0x300) % 16
|
||
private fun diacriticalMarksIndexY(c: CodePoint) = (c - 0x300) / 16
|
||
|
||
private fun polytonicGreekIndexX(c: CodePoint) = (c - 0x1F00) % 16
|
||
private fun polytonicGreekIndexY(c: CodePoint) = (c - 0x1F00) / 16
|
||
|
||
private fun extCIndexX(c: CodePoint) = (c - 0x2C60) % 16
|
||
private fun extCIndexY(c: CodePoint) = (c - 0x2C60) / 16
|
||
|
||
private fun extDIndexX(c: CodePoint) = (c - 0xA720) % 16
|
||
private fun extDIndexY(c: CodePoint) = (c - 0xA720) / 16
|
||
|
||
/*
|
||
#!/usr/bin/python3
|
||
|
||
s = "charseq"
|
||
|
||
for c in s:
|
||
#print(ord(c))
|
||
print("0x{0:x}".format(ord(c)))
|
||
*/ // acegijmnopqrsuvwxyzɱɳʙɾɽʒʂʐʋɹɻɥɟɡɢʛȵɲŋɴʀɕʑçʝxɣχʁʜʍɰʟɨʉɯuʊøɘɵɤəɛœɜɞʌɔæɐɶɑɒɚɝɩɪʅʈʏʞⱥⱦⱱⱳⱴⱶⱷⱸⱺⱻꜥꜩꜫꜭꜯꜰꜱꜳꜵꜷꜹꜻꜽꜿꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝫꝯꝳꝴꝵꝶꝷꝺꝼꝿꞁꞃꞅꞇꞑꞓꞔꞛꞝꞟꞡꞥꞧꞩꞮꞷꟺ\uA7AF\uA7B9\uA7C3\uA7CA
|
||
private val lowHeightLetters = intArrayOf(0x61,0x63,0x65,0x67,0x69,0x6a,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x75,0x76,0x77,0x78,0x79,0x7a,0x271,0x273,0x299,0x27e,0x27d,0x292,0x282,0x290,0x28b,0x279,0x27b,0x265,0x25f,0x261,0x262,0x29b,0x235,0x272,0x14b,0x274,0x280,0x255,0x291,0xe7,0x29d,0x78,0x263,0x3c7,0x281,0x29c,0x28d,0x270,0x29f,0x268,0x289,0x26f,0x75,0x28a,0xf8,0x258,0x275,0x264,0x259,0x25b,0x153,0x25c,0x25e,0x28c,0x254,0xe6,0x250,0x276,0x251,0x252,0x25a,0x25d,0x269,0x26a,0x285,0x288,0x28f,0x29e,0x2c65,0x2c66,0x2c71,0x2c73,0x2c74,0x2c76,0x2c77,0x2c78,0x2c7a,0x2c7b,0xa725,0xa729,0xa72b,0xa72d,0xa72f,0xa730,0xa731,0xa733,0xa735,0xa737,0xa739,0xa73b,0xa73d,0xa73f,0xa74b,0xa74d,0xa74f,0xa751,0xa753,0xa755,0xa757,0xa759,0xa75b,0xa75d,0xa75f,0xa761,0xa76b,0xa76f,0xa773,0xa774,0xa775,0xa776,0xa777,0xa77a,0xa77c,0xa77f,0xa781,0xa783,0xa785,0xa787,0xa791,0xa793,0xa794,0xa79b,0xa79d,0xa79f,0xa7a1,0xa7a5,0xa7a7,0xa7a9,0xa7ae,0xa7b7,0xa7fa,0xa7af,0xa7b9,0xa7c3,0xa7ca).toSortedSet()
|
||
// TŢŤƬƮȚͲΤТҬᛏṪṬṮṰⲦϮϯⴶꚌꚐᎢᛠꓔ
|
||
private val kernTees = intArrayOf(0x54,0x162,0x164,0x1ac,0x1ae,0x21a,0x372,0x3a4,0x422,0x4ac,0x16cf,0x1e6a,0x1e6c,0x1e6e,0x1e70,0x2ca6,0x3ee,0x3ef,0x2d36,0xa68c,0xa690,0x13a2,0x16e0,0xa4d4).toSortedSet()
|
||
// ŦȾYÝŶŸɎΎΫΥҮҰᛉᛘẎỲỴỶỸὙὛὝὟῪΎꓬ
|
||
private val kernYees = intArrayOf(0x166,0x23e,0x59,0xdd,0x176,0x178,0x24e,0x38e,0x3ab,0x3a5,0x4ae,0x4b0,0x16c9,0x16d8,0x1e8e,0x1ef2,0x1ef4,0x1ef6,0x1ef8,0x1f59,0x1f5b,0x1f5d,0x1f5f,0x1fea,0x1feb,0xa4ec).toSortedSet()
|
||
// VṼṾⱯⴸꓦꓯꝞ
|
||
private val kernVees = intArrayOf(0x56,0x1e7c,0x1e7e,0x2c6f,0x2d38,0xa4e6,0xa4ef,0xa75e).toSortedSet()
|
||
// AÀÁÂÃÄÅĀĂĄǍǞǠǺȀȂȦɅΆΑΛАДЛѦӅӐӒԮḀẠẢẤẦẨẪẬẮẰẲẴẶἈἉἊἋἌἍἎἏᾸᾹᾺΆꓥꓮꙞꙢꙤꚀꚈꜲ
|
||
private val kernAyes = intArrayOf(0x41,0xc0,0xc1,0xc2,0xc3,0xc4,0xc5,0x100,0x102,0x104,0x1cd,0x1de,0x1e0,0x1fa,0x200,0x202,0x226,0x245,0x386,0x391,0x39b,0x410,0x414,0x41b,0x466,0x4c5,0x4d0,0x4d2,0x52e,0x1e00,0x1ea0,0x1ea2,0x1ea4,0x1ea6,0x1ea8,0x1eaa,0x1eac,0x1eae,0x1eb0,0x1eb2,0x1eb4,0x1eb6,0x1f08,0x1f09,0x1f0a,0x1f0b,0x1f0c,0x1f0d,0x1f0e,0x1f0f,0x1fb8,0x1fb9,0x1fba,0x1fbb,0xa4e5,0xa4ee,0xa65e,0xa662,0xa664,0xa680,0xa688,0xa732).toSortedSet()
|
||
// LĹĻĽĿŁʟᏞᴌḶḸḺḼꓡꓕꝆꝈꞭꞱꮮւևⳐⳑԼⱢⱠ
|
||
private val kernElls = intArrayOf(0x4c,0x139,0x13b,0x13d,0x13f,0x141,0x29f,0x13de,0x1d0c,0x1e36,0x1e38,0x1e3a,0x1e3c,0xa4e1,0xa4d5,0xa746,0xa748,0xa7ad,0xa7b1,0xabae,0x582,0x587,0x2cd0,0x2cd1,0x53c,0x2c62,0x2c60).toSortedSet()
|
||
// ΓЃГҐҒӶӺᎱᚨᚩᚪᚫᚹᛇᛚᛛᛢᛮⲄꓩꞄ
|
||
private val kernGammas = intArrayOf(0x393,0x403,0x413,0x490,0x492,0x4f6,0x4fa,0x13b1,0x16a8,0x16a9,0x16aa,0x16ab,0x16b9,0x16c7,0x16da,0x16db,0x16e2,0x16ee,0x2c84,0xa4e9,0xa784).toSortedSet()
|
||
// JĴɹɺɻͿᛇᴊᎫᏗꓕꓙꞱꭻꮧ
|
||
private val kernJays = intArrayOf(0x4a,0x134,0x279,0x27a,0x27b,0x37f,0x16c7,0x1d0a,0x13ab,0x13d7,0xa4d5,0xa4d9,0xa7b1,0xab7b,0xaba7).toSortedSet()
|
||
|
||
private val kernTee = -2
|
||
private val kernYee = -1
|
||
private val kernAV = -1
|
||
|
||
private fun getKerning(prevChar: CodePoint, thisChar: CodePoint): Int {
|
||
return if (prevChar in lowHeightLetters) {
|
||
return if (thisChar in kernTees) kernTee // lh - T
|
||
else if (thisChar in kernYees) kernYee // lh - Y
|
||
else 0
|
||
}
|
||
else if (prevChar in kernElls) {
|
||
return if (thisChar in kernTees) kernTee // L - T
|
||
else if (thisChar in kernVees) kernYee // L - V
|
||
else if (thisChar in kernYees) kernYee // L - Y
|
||
else 0
|
||
}
|
||
else if (prevChar in kernTees) {
|
||
return if (thisChar in lowHeightLetters) kernTee // T - lh
|
||
else if (thisChar in kernJays) kernTee // T - J
|
||
else if (thisChar in kernAyes) kernYee // T - A
|
||
else 0
|
||
}
|
||
else if (prevChar in kernYees) {
|
||
return if (thisChar in lowHeightLetters) kernYee // Y - lh
|
||
else if (thisChar in kernAyes) kernYee // Y - A
|
||
else if (thisChar in kernJays) kernYee // Y - J
|
||
else 0
|
||
}
|
||
else if (prevChar in kernAyes) {
|
||
return if (thisChar in kernVees) kernAV // A - V
|
||
else if (thisChar in kernTees) kernAV // A - T
|
||
else if (thisChar in kernYees) kernYee // A - Y
|
||
else 0
|
||
}
|
||
else if (prevChar in kernVees) {
|
||
return if (thisChar in kernAyes) kernAV // V - A
|
||
else if (thisChar in kernJays) kernAV // V - J
|
||
else 0
|
||
}
|
||
else if (prevChar in kernGammas) {
|
||
return if (thisChar in kernAyes) kernYee // Γ - Α
|
||
else if (thisChar in lowHeightLetters) kernTee // Γ - lh
|
||
else if (thisChar in kernJays) kernTee // Γ - J
|
||
else 0
|
||
}
|
||
else 0
|
||
}
|
||
|
||
|
||
val charsetOverrideDefault = Character.toChars(CHARSET_OVERRIDE_DEFAULT)
|
||
val charsetOverrideBulgarian = Character.toChars(CHARSET_OVERRIDE_BG_BG)
|
||
val charsetOverrideSerbian = Character.toChars(CHARSET_OVERRIDE_SR_SR)
|
||
fun toColorCode(argb4444: Int): String = Character.toChars(0x100000 + argb4444).toColCode()
|
||
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.toColCode(): String = "${this[0]}${this[1]}"
|
||
|
||
val noColorCode = toColorCode(0x0000)
|
||
}
|
||
|
||
}
|