From 3500f17e08cca28e0778e603ba0a34fd1f1cc700 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 21 May 2024 16:47:06 +0900 Subject: [PATCH] fix: quirks with getting width of blocks and typesetting --- .../torvald/terrarumsansbitmap/MovableType.kt | 107 +++--------- .../gdx/TerrarumSansBitmap.kt | 163 +++++++++++++++++- .../gdx/TerrarumTypewriterBitmap.kt | 4 +- 3 files changed, 183 insertions(+), 91 deletions(-) diff --git a/src/net/torvald/terrarumsansbitmap/MovableType.kt b/src/net/torvald/terrarumsansbitmap/MovableType.kt index 6088f7a..57caa6d 100644 --- a/src/net/torvald/terrarumsansbitmap/MovableType.kt +++ b/src/net/torvald/terrarumsansbitmap/MovableType.kt @@ -8,7 +8,12 @@ import net.torvald.terrarumsansbitmap.gdx.CodePoint import net.torvald.terrarumsansbitmap.gdx.CodepointSequence import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap 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.getHash +import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap.Companion.glueCharToGlueSize import kotlin.math.* import kotlin.properties.Delegates @@ -87,7 +92,7 @@ class MovableType( fun dequeue() = boxes.removeFirst() fun addHyphenatedTail(box: NoTexGlyphLayout) = boxes.add(0, box) fun addToSlug(box: NoTexGlyphLayout) { - val nextPosX = (slug.lastOrNull()?.getEndPos() ?: 0) + val nextPosX = slug.getSlugEndPos() slug.add(Block(nextPosX, box)) slugWidth += box.width @@ -140,6 +145,9 @@ class MovableType( slug = ArrayList() slugWidth = 0 + + +// println("Frozen slug: ${frozen.toReadable()}") } /////////////////////////////////////////////////////////////////////////////////////////////// @@ -153,7 +161,7 @@ class MovableType( slug.removeLastOrNull() } - var slugWidth = (slug.lastOrNull()?.getEndPos() ?: 0) - exdentSize + var slugWidth = slug.getSlugEndPos() - exdentSize if (slug.isNotEmpty() && slug.last().block.penultimateCharOrNull != null && hangable.contains(slug.last().block.penultimateCharOrNull)) slugWidth -= hangWidth else if (slug.isNotEmpty() && slug.last().block.penultimateCharOrNull != null && hangableFW.contains(slug.last().block.penultimateCharOrNull)) @@ -174,7 +182,7 @@ class MovableType( }*/ // add the box to the slug copy - val nextPosX = (slug.lastOrNull()?.getEndPos() ?: 0) + val nextPosX = slug.getSlugEndPos() slug.add(Block(nextPosX, box)) var slugWidth = slugWidth + box.width - exdentSize @@ -198,7 +206,7 @@ class MovableType( // - the word is too short (5 chars or less) // - the word is pre-hyphenated (ends with hyphen-null) val glyphCount = box.text.count { it in 32..0xFFF6F && it !in 0xFFF0..0xFFFF } - if (glyphCount <= (if (paperWidth < 350) 4 else if (paperWidth < 480) 5 else 6) || box.text.penultimate() == 0x2D) + if (glyphCount <= (if (paperWidth < 350) 3 else if (paperWidth < 480) 4 else 5) || box.text.penultimate() == 0x2D) return Triple(Double.POSITIVE_INFINITY, 2147483647, null) val slug = slug.toMutableList() // ends with a glue @@ -222,7 +230,7 @@ class MovableType( return Triple(Double.POSITIVE_INFINITY, 2147483647, null) // add the hyphHead to the slug copy - val nextPosX = (slug.lastOrNull()?.getEndPos() ?: 0) + val nextPosX = slug.getSlugEndPos() slug.add(Block(nextPosX, hyphHead)) // now ends with 'word-' (but not in Hangul) val hasHyphen = hyphHead.penultimateCharOrNull == 0x2D @@ -322,7 +330,7 @@ class MovableType( if (badnessH.isInfinite() && badnessW.isInfinite() && badnessT.isInfinite()) { throw Error( - "Typesetting failed: badness of all three strategies diverged to infinity\ntext (${slug.size} tokens): ${ + "Typesetting failed: badness of all three strategies diverged to infinity. Try adding white spaces to the text.\nThe text (${slug.size} tokens): ${ slug.map { it.block.text }.filter { it.isNotGlue() } .joinToString(" ") { it.toReadable() } }" @@ -586,9 +594,9 @@ class MovableType( var cM: CodePoint? = null var glue = 0 - fun getControlHeader(row: Int, word: Int): CodepointSequence { + fun getControlHeader(row: Int, word: Int): List { val index = row * 65536 or word - val ret = CodepointSequence(controlCharList.filter { index > it.second }.map { it.first }) + val ret = controlCharList.filter { index > it.second }.map { it.first } return ret } @@ -832,13 +840,6 @@ class MovableType( return lines } - private fun java.util.ArrayList.penultimate(): E { - return this[this.size - 2] - } - private fun java.util.ArrayList.penultimateOrNull(): E? { - return this.getOrNull(this.size - 2) - } - private fun penaliseWidening(score: Int, availableGlues: Double): Double = 100.0 * (score / availableGlues).pow(3.0) // pow(score.toDouble(), 2.0) @@ -873,16 +874,6 @@ class MovableType( private fun CodePoint?.isThaiConso() = if (this == null) false else this in 0x0E01..0x0E2F private fun CodePoint?.isThaiVowel() = if (this == null) false else (this in 0x0E30..0x0E3E || this in 0x0E40..0x0E4E) - private fun CodepointSequence.isGlue() = this.size == 1 && (this[0] == ZWSP || this[0] in 0xFFFE0..0xFFFFF) - private fun CodepointSequence.isNotGlue() = !this.isGlue() - private fun CodepointSequence.isZeroGlue() = this.size == 1 && (this[0] == ZWSP) - private fun CodePoint.glueCharToGlueSize() = when (this) { - ZWSP -> 0 - in 0xFFFE0..0xFFFEF -> -(this - 0xFFFE0 + 1) - in 0xFFFF0..0xFFFFF -> this - 0xFFFF0 + 1 - else -> throw IllegalArgumentException() - } - private fun CodePoint?.isWesternPunctOrQuotes() = if (this == null) false else (westernPuncts.contains(this) || quots.contains(this)) private fun CodePoint?.isParens() = if (this == null) false else parens.contains(this) private fun CodePoint?.isParenOpen() = if (this == null) false else parenOpen.contains(this) @@ -893,7 +884,7 @@ class MovableType( /** * Hyphenates the word at the middle ("paragraph" -> "para-graph") - * + * * @return left word ("para-"), right word ("graph") */ private fun CodepointSequence.hyphenate(font: TerrarumSansBitmap, optimalCuttingPointInPx: Int): Pair { @@ -997,7 +988,7 @@ class MovableType( 0x20 to 4, 0x2009 to 2, 0x200A to 1, - 0x200B to 0, + ZWSP to 0, 0x3000 to 16, 0xF0520 to 7, // why???? ) @@ -1010,10 +1001,6 @@ class MovableType( private val parenOpen = listOf(0x28,0x5B,0x7B).toSortedSet().also { it.addAll(cjparenStarts) } private val parenClose = listOf(0x29,0x5D,0x7D).toSortedSet().also { it.addAll(cjparenEnds) } - const val ZWSP = 0x200B - const val SHY = 0xAD - const val NBSP = 0xA0 - const val OBJ = 0xFFFC const val GLUE_POSITIVE_ONE = 0xFFFF0 const val GLUE_POSITIVE_SIXTEEN = 0xFFFFF const val GLUE_NEGATIVE_ONE = 0xFFFE0 @@ -1023,54 +1010,6 @@ class MovableType( private inline fun Int.codepointToString() = Character.toChars(this).toSurrogatedString() - fun CodepointSequence.toReadable() = this.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) - " " - else if (it in GLUE_NEGATIVE_ONE..GLUE_POSITIVE_SIXTEEN) - " " - 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()) - } - - private fun List>.debugprint() { println("Tokenised (${this.size} lines):") this.forEach { @@ -1139,13 +1078,15 @@ class MovableType( val input = this.filter { it.block.text.isNotGlue() } if (input.isEmpty()) return out +// println("freezeIntoCodepointSequence ${input.joinToString { "${it.posX}..${it.getEndPos()}" }}") + // process line indents if (input.first().posX > 0) out.addAll(input.first().posX.glueSizeToGlueChars()) // process blocks input.forEachIndexed { index, it -> - val posX = it.posX + 1 - font.interchar * 2 + val posX = it.posX - font.interchar * 2 val prevEndPos = if (index == 0) 0 else input[index-1].getEndPos() if (index > 0 && posX != prevEndPos) { out.addAll((posX - prevEndPos).glueSizeToGlueChars()) @@ -1187,7 +1128,7 @@ class MovableType( // process blocks input.forEachIndexed { index, it -> - val posX = it.posX + 1 - font.interchar * 2 + val posX = it.posX - font.interchar * 2 val prevEndPos = if (index == 0) 0 else input[index-1].getEndPos() if (index > 0 && posX != prevEndPos) { out += posX - prevEndPos @@ -1199,6 +1140,10 @@ class MovableType( inline fun Boolean.toInt(shift: Int = 0) = if (this) 1.shl(shift) else 0 + private fun List.getSlugEndPos(): Int { + return this.lastOrNull()?.getEndPos() ?: 0 + } } // end of companion object } + diff --git a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt index 3451d39..447718d 100755 --- a/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt +++ b/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt @@ -33,18 +33,133 @@ 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.collections.ArrayList -import kotlin.collections.HashMap import kotlin.math.roundToInt import kotlin.math.sign -typealias CodepointSequence = ArrayList +class CodepointSequence { + private val data = ArrayList() + + constructor() + + constructor(chars: Collection) { + data.addAll(chars) + } + + 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) + fun add(index: Int, char: CodePoint) = data.add(index, char) + operator fun set(index: Int, char: CodePoint) { + data[index] = char + } + fun add(char: CodePoint) = data.add(char) + fun addAll(chars: Collection) = data.addAll(chars) + fun addAll(cs: CodepointSequence) = data.addAll(cs.data) + fun removeAt(index: Int) = data.removeAt(index) + fun remove(char: CodePoint) = data.remove(char) + operator fun get(index: Int) = data[index] + fun getOrNull(index: Int) = data.getOrNull(index) + fun getOrElse(index: Int, action: (Int) -> CodePoint) = data.getOrElse(index, action) + fun isEmpty() = data.isEmpty() + fun isNotEmpty() = data.isNotEmpty() + fun count(predicate: (CodePoint) -> Boolean) = data.count(predicate) + fun addAll(index: Int, elements: Collection) = data.addAll(index, elements) + fun addAll(index: Int, elements: CodepointSequence) = data.addAll(index, elements.data) + fun subList(fromIndex: Int, toIndex: Int) = data.subList(fromIndex, toIndex) + fun slice(indices: IntRange) = data.slice(indices) + + 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) + " " + else if (it in GLUE_NEGATIVE_ONE..GLUE_POSITIVE_SIXTEEN) + " " + 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 @@ -330,7 +445,7 @@ class TerrarumSansBitmap( private val offsetCustomSym = (H - SIZE_CUSTOM_SYM) / 2 private var flagFirstRun = true - private var textBuffer = CodepointSequence(256) + private var textBuffer = CodepointSequence() private lateinit var tempLinotype: Texture @@ -918,19 +1033,38 @@ class TerrarumSansBitmap( for (i in 0xFFF70..0xFFF9F) { glyphProps[i] = GlyphProps(0) } + + glyphProps[ZWNJ] = GlyphProps(0) + glyphProps[ZWJ] = GlyphProps(0) + glyphProps[ZWSP] = 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[0xA0] = FIXED_BLOCK_1 + (spaceWidth - 1) + 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 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) { @@ -949,7 +1083,7 @@ class TerrarumSansBitmap( * @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: List): Posmap { + private fun buildPosMap(str: CodepointSequence): Posmap { val posXbuffer = IntArray(str.size + 1) { 0 } val posYbuffer = IntArray(str.size) { 0 } @@ -1130,7 +1264,7 @@ class TerrarumSansBitmap( val penultCharProp = glyphProps[str[nonDiacriticCounter]] ?: (if (errorOnUnknownChar) throw throw InternalError("No GlyphProps for char '${str[nonDiacriticCounter]}' " + "(${str[nonDiacriticCounter].charInfo()})") else nullProp) - posXbuffer[posXbuffer.lastIndex] = 1 + posXbuffer[posXbuffer.lastIndex - 1] + // adding 1 to house the shadow + 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 @@ -2221,6 +2355,13 @@ class TerrarumSansBitmap( 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 @@ -2467,8 +2608,12 @@ class TerrarumSansBitmap( internal fun Int.charInfo() = "U+${this.toString(16).padStart(4, '0').toUpperCase()}: ${Character.getName(this)}" - private const val ZWNJ = 0x200C - private const val ZWJ = 0x200D + const val ZWNJ = 0x200C + const val ZWJ = 0x200D + const val ZWSP = 0x200B + 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 diff --git a/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt b/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt index 020af7b..42218d9 100644 --- a/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt +++ b/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt @@ -278,7 +278,7 @@ class TerrarumTypewriterBitmap( private val pixmapOffsetY = 10 private val linotypePad = 16 private var flagFirstRun = true - private @Volatile var textBuffer = CodepointSequence(256) + private @Volatile var textBuffer = CodepointSequence() private @Volatile lateinit var tempLinotype: Texture private var nullProp = GlyphProps(15) @@ -396,6 +396,8 @@ class TerrarumTypewriterBitmap( } + private fun buildPosMap(str: CodepointSequence) = buildPosMap(str.toList()) + /** * posXbuffer's size is greater than the string, last element marks the width of entire string. */