fix: quirks with getting width of blocks and typesetting

This commit is contained in:
minjaesong
2024-05-21 16:47:06 +09:00
parent 7c8a1be3e5
commit 3500f17e08
3 changed files with 183 additions and 91 deletions

View File

@@ -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<Int> {
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 <E> java.util.ArrayList<E>.penultimate(): E {
return this[this.size - 2]
}
private fun <E> java.util.ArrayList<E>.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)
@@ -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)
" <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())
}
private fun List<ArrayList<CodepointSequence>>.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<Block>.getSlugEndPos(): Int {
return this.lastOrNull()?.getEndPos() ?: 0
}
} // end of companion object
}

View File

@@ -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<CodePoint>
class CodepointSequence {
private val data = ArrayList<CodePoint>()
constructor()
constructor(chars: Collection<CodePoint>) {
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<CodePoint>) = 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<CodePoint>) = 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)
" <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
@@ -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<Int>): 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

View File

@@ -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.
*/