--- /dev/null
+package info.mechyrdia.lore
+
+import com.jaredrummler.fontreader.truetype.FontFileReader
+import com.jaredrummler.fontreader.truetype.TTFFile
+import com.jaredrummler.fontreader.util.GlyphSequence
+import info.mechyrdia.data.*
+import info.mechyrdia.route.KeyedEnumSerializer
+import info.mechyrdia.yieldThread
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.awt.Font
+import java.awt.Shape
+import java.awt.geom.AffineTransform
+import java.awt.geom.GeneralPath
+import java.awt.geom.PathIterator
+import java.awt.image.BufferedImage
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.nio.IntBuffer
+import kotlin.properties.ReadOnlyProperty
+
+@Serializable(with = TextAlignmentSerializer::class)
+enum class TextAlignment {
+ LEFT {
+ override fun processWidth(widthDiff: Int): Int {
+ return 0
+ }
+ },
+ CENTER {
+ override fun processWidth(widthDiff: Int): Int {
+ return widthDiff / 2
+ }
+ },
+ RIGHT {
+ override fun processWidth(widthDiff: Int): Int {
+ return widthDiff
+ }
+ };
+
+ abstract fun processWidth(widthDiff: Int): Int
+}
+
+object TextAlignmentSerializer : KeyedEnumSerializer<TextAlignment>(TextAlignment.entries)
+
+data class SvgDoc(
+ val width: Double,
+ val height: Double,
+ val viewBoxX: Double,
+ val viewBoxY: Double,
+ val viewBoxW: Double,
+ val viewBoxH: Double,
+ val path: SvgPath
+)
+
+data class SvgPath(
+ val d: String,
+ val fillRule: String
+)
+
+fun <T, C : XmlTagConsumer<T>> C.svg(svgDoc: SvgDoc) = declaration(standalone = false)
+ .defaultXmlns("http://www.w3.org/2000/svg")
+ .root(
+ "svg",
+ attributes = mapOf(
+ "width" to svgDoc.width.xmlValue,
+ "height" to svgDoc.height.xmlValue,
+ "viewBox" to listOf(
+ svgDoc.viewBoxX,
+ svgDoc.viewBoxY,
+ svgDoc.viewBoxW,
+ svgDoc.viewBoxH,
+ ).joinToString(separator = " ") { it.xmlValue }
+ )
+ ) { "path"(attributes = mapOf("d" to svgDoc.path.d, "fill-rule" to svgDoc.path.fillRule)) }
+
+object MechyrdiaSansFont {
+ private val logger: Logger = LoggerFactory.getLogger(MechyrdiaSansFont::class.java)
+
+ suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): SvgDoc {
+ val (file, font) = getFont(bold, italic)
+ val shape = layoutText(text, file, font, align)
+ return createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0)
+ }
+
+ private val fontsRoot = StoragePath("fonts")
+ private fun fontFile(name: String) = fontsRoot / "$name.ttf"
+ private suspend fun loadFont(fontFile: StoragePath): Pair<TTFFile, Font>? {
+ val bytes = FileStorage.instance.readFile(fontFile) ?: return null
+
+ return withContext(Dispatchers.IO) {
+ val file = TTFFile(true, true)
+ file.readFont(FontFileReader(ByteArrayInputStream(bytes)))
+
+ val font = Font
+ .createFont(Font.TRUETYPE_FONT, ByteArrayInputStream(bytes))
+ .deriveFont(file.unitsPerEm.toFloat())
+
+ file to font
+ }
+ }
+
+ private fun loadedFont(fontName: String): ReadOnlyProperty<Any?, suspend () -> Pair<TTFFile, Font>?> {
+ return storedData(fontFile(fontName), ::loadFont)
+ }
+
+ private val mechyrdiaSans by loadedFont("mechyrdia-sans")
+ private val mechyrdiaSansB by loadedFont("mechyrdia-sans-bold")
+ private val mechyrdiaSansI by loadedFont("mechyrdia-sans-italic")
+ private val mechyrdiaSansBI by loadedFont("mechyrdia-sans-bold-italic")
+
+ private val mechyrdiaSansFonts = listOf(mechyrdiaSans, mechyrdiaSansI, mechyrdiaSansB, mechyrdiaSansBI)
+ private suspend fun getFont(bold: Boolean, italic: Boolean): Pair<TTFFile, Font> {
+ return mechyrdiaSansFonts[(if (bold) 2 else 0) + (if (italic) 1 else 0)]()!!
+ }
+
+ private fun TTFFile.getGlyph(cp: Int): Int {
+ return try {
+ unicodeToGlyph(cp)
+ } catch (ex: IOException) {
+ 0
+ }
+ }
+
+ private fun String.toCodePointSequence() = sequence {
+ val l = length
+ var i = 0
+ while (i < l) {
+ val cp = Character.codePointAt(this@toCodePointSequence, i)
+ i += if (Character.isSupplementaryCodePoint(cp)) 2 else 1
+ yield(cp)
+ }
+ }
+
+ private fun String.toCodePointArray(): IntArray {
+ val iter = toCodePointSequence().iterator()
+
+ return IntArray(codePointCount(0, length)) { _ ->
+ assert(iter.hasNext())
+ iter.next()
+ }
+ }
+
+ private fun TTFFile.getGlyphs(str: String): GlyphSequence {
+ val codes = str.toCodePointArray()
+ val glyphs = IntArray(codes.size) { i -> getGlyph(codes[i]) }
+
+ return GlyphSequence(IntBuffer.wrap(codes), IntBuffer.wrap(glyphs), null)
+ }
+
+ private fun TTFFile.getBasicWidths(glyphSequence: GlyphSequence): IntArray {
+ return IntArray(glyphSequence.glyphCount) { i ->
+ if (i == 0)
+ mtx[glyphSequence.getGlyph(i)].wx
+ else {
+ val prev = glyphSequence.getGlyph(i - 1)
+ val curr = glyphSequence.getGlyph(i)
+ (rawKerning[prev]?.get(curr) ?: 0) + mtx[curr].wx
+ }
+ }
+ }
+
+ private fun TTFFile.getGlyphPositions(glyphSequence: GlyphSequence, widths: IntArray): Array<IntArray> {
+ val adjustments = Array(glyphSequence.glyphCount) { IntArray(4) }
+ gpos.position(glyphSequence, "latn", "*", 0, widths, adjustments)
+
+ // I don't know why this is necessary,
+ // but it gives me the results I want.
+ for (adjustment in adjustments) {
+ adjustment[0] *= 2
+ adjustment[1] *= 2
+ adjustment[2] *= 2
+ adjustment[3] *= 2
+ }
+
+ return adjustments
+ }
+
+ private fun getWidth(widths: IntArray, glyphPositions: Array<IntArray>): Int {
+ return widths.zip(glyphPositions) { width, pos -> width + pos[2] }.sum()
+ }
+
+ private fun layoutText(text: String, file: TTFFile, font: Font, align: TextAlignment): Shape {
+ val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB)
+ val g2d = img.createGraphics()
+ try {
+ val charHolder = CharArray(2)
+ val lineHeight = file.rawLowerCaseAscent - file.rawLowerCaseDescent
+
+ val lines = text.split("\r\n", "\n", "\r")
+ val lineGlyphs = lines.map { file.getGlyphs(it) }
+ val lineBasics = lineGlyphs.map { file.getBasicWidths(it) }
+ val lineAdjust = lineGlyphs.zip(lineBasics) { glyphs, widths -> file.getGlyphPositions(glyphs, widths) }
+ val lineWidths = lineBasics.zip(lineAdjust) { width, adjust -> getWidth(width, adjust) }
+ val blockWidth = lineWidths.max()
+ var ly = 0
+
+ yieldThread()
+
+ val shape = GeneralPath()
+ val tf = AffineTransform()
+ for ((li, line) in lines.withIndex()) {
+ if (line.isNotBlank()) {
+ val lineWidth = lineWidths[li]
+ val lx = align.processWidth(blockWidth - lineWidth)
+
+ var cx = 0
+ var cy = 0
+
+ val basicAdv = lineBasics[li]
+ val adjusted = lineAdjust[li]
+ val glyphSeq = lineGlyphs[li]
+ for ((ci, codePoint) in glyphSeq.getCharacterArray(false).withIndex()) {
+ val length = Character.toChars(codePoint, charHolder, 0)
+ val glyph = font.layoutGlyphVector(g2d.fontRenderContext, charHolder, 0, length, Font.LAYOUT_LEFT_TO_RIGHT)
+ val glyphShape = glyph.outline as GeneralPath
+ val glyphShift = adjusted[ci]
+
+ tf.setToTranslation((lx + cx + glyphShift[0]).toDouble(), (ly + cy + glyphShift[1]).toDouble())
+ shape.append(glyphShape.getPathIterator(tf), false)
+
+ cx += glyphShift[2] + basicAdv[ci]
+ cy += glyphShift[3]
+ }
+ }
+
+ ly += lineHeight
+
+ yieldThread()
+ }
+
+ return shape
+ } catch (ex: Exception) {
+ logger.error("Error converting text $text to font shape", ex)
+ return GeneralPath()
+ } finally {
+ g2d.dispose()
+ }
+ }
+
+ private fun createSvgDocument(shape: Shape, scale: Double, padding: Double = 0.0): SvgDoc {
+ val viewBox = shape.bounds2D
+ val vBoxPad = padding / scale
+ val sizePad = padding * 2
+
+ val path = shape.calculateSvgPath()
+
+ return SvgDoc(
+ width = (viewBox.width * scale) + sizePad,
+ height = (viewBox.height * scale) + sizePad,
+ viewBoxX = viewBox.minX - vBoxPad,
+ viewBoxY = viewBox.minY - vBoxPad,
+ viewBoxW = viewBox.width + (vBoxPad * 2),
+ viewBoxH = viewBox.height + (vBoxPad * 2),
+ path = path
+ )
+ }
+
+ private fun Shape.calculateSvgPath(): SvgPath {
+ val iterator = getPathIterator(null)
+ val d = buildString {
+ val coords = DoubleArray(6)
+ var isFirst = true
+
+ while (!iterator.isDone) {
+ if (isFirst)
+ isFirst = false
+ else
+ append(' ')
+
+ when (val segment = iterator.currentSegment(coords)) {
+ PathIterator.SEG_MOVETO -> {
+ append("M ${coords[0]},${coords[1]}")
+ }
+
+ PathIterator.SEG_LINETO -> {
+ append("L ${coords[0]},${coords[1]}")
+ }
+
+ PathIterator.SEG_QUADTO -> {
+ append("Q ${coords[0]},${coords[1]} ${coords[2]},${coords[3]}")
+ }
+
+ PathIterator.SEG_CUBICTO -> {
+ append("C ${coords[0]},${coords[1]} ${coords[2]},${coords[3]} ${coords[4]},${coords[5]}")
+ }
+
+ PathIterator.SEG_CLOSE -> {
+ append("Z")
+ }
+
+ else -> error("Invalid segment type $segment")
+ }
+
+ iterator.next()
+
+ yieldThread()
+ }
+ }
+
+ val fillRule = when (val winding = iterator.windingRule) {
+ PathIterator.WIND_EVEN_ODD -> "evenodd"
+ PathIterator.WIND_NON_ZERO -> "nonzero"
+ else -> error("Invalid winding rule $winding")
+ }
+
+ return SvgPath(d, fillRule)
+ }
+}
+
+object TylanAlphabetFont {
+ private val allowedTranslitCharacters = setOf(
+ ' ', '\r', '\n', '\t',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+',
+ '[', '{', '}', ']', '\\', '|', '/', '<',
+ '.', ',', ':', ';', '\'', '"', '?', '>',
+ )
+
+ private val replacements = listOf(
+ Regex("([0-9xy]+)(?![\\s0-9xy])") to "$1 ",
+ Regex("(?<![\\s0-9xy])([0-9xy]+)") to " $1",
+
+ Regex("([a-pr-v'])m(?![\\w'])") to "$1A",
+ Regex("([a-pr-v'])n(?![\\w'])") to "$1E",
+ Regex("([a-ps-v'])r(?![\\w'])") to "$1H",
+ Regex("([ac-fh-jl-or-v'])s(?![\\w'])") to "$1I",
+ Regex("([a-pr-v'])t(?![\\w'])") to "$1Q",
+
+ Regex("x") to "M",
+ Regex("y") to "N",
+ Regex("[qw-z]") to "",
+
+ Regex("ch") to "C",
+ Regex("dh") to "D",
+ Regex("sh") to "S",
+ Regex("th") to "T",
+
+ Regex("bs") to "B",
+ Regex("gs") to "G",
+ Regex("ks") to "K",
+ Regex("ps") to "P",
+
+ Regex("b'([Is])") to "b$1",
+ Regex("g'([Is])") to "g$1",
+ Regex("k'([Is])") to "k$1",
+ Regex("p'([Is])") to "p$1",
+
+ Regex("ff") to "F",
+ Regex("fv") to "V",
+ Regex("hj") to "J",
+ Regex("lh") to "L",
+ Regex("ll") to "L",
+ Regex("hl") to "L",
+ Regex("rh") to "R",
+ Regex("rr") to "R",
+ Regex("hr") to "R",
+ Regex("d'h") to "dh",
+ Regex("l'h") to "lh",
+ Regex("l'J") to "lJ",
+ Regex("r'h") to "rh",
+ Regex("r'J") to "rJ",
+ Regex("s'h") to "sh",
+ Regex("t'h") to "th",
+ Regex("h'l") to "hl",
+ Regex("h'r") to "hr",
+ Regex("B'h") to "Bh",
+ Regex("G'h") to "Gh",
+ Regex("K'h") to "Kh",
+ Regex("P'h") to "Ph",
+ Regex("c(?!h)") to "",
+
+ Regex("ae") to "W",
+ Regex("au") to "X",
+ Regex("ea") to "Y",
+ Regex("ei") to "Z",
+ Regex("eo") to "w",
+ Regex("eu") to "x",
+ Regex("oa") to "y",
+ Regex("ou") to "z",
+ Regex("oe") to "O",
+ Regex("ui") to "U",
+
+ Regex("i([aeiouOUw-zW-Z])") to "ij$1",
+ Regex("^([aeiouOUw-zW-Z])") to "'$1",
+ Regex("([^'BCDFGJKLPR-TVbdf-hj-npr-tv])([aeiouOUw-zW-Z])") to "$1'$2",
+ Regex("([aeiouOUw-zW-Z])([aeiouOUw-zW-Z])") to "$1'$2",
+ )
+
+ fun tylanToFontAlphabet(tylan: String) = replacements.fold(tylan.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) ->
+ partial.replace(regex, replacement)
+ }
+}
+
+object PokhwalishAlphabetFont {
+ private val allowedTranslitCharacters = setOf(
+ ' ', '\r', '\n', '\t',
+ 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'y', 'z',
+ '.', ',', '\'', '?', '!',
+ )
+
+ private val replacements = listOf(
+ // Vowels
+ Regex("aa") to "A",
+ Regex("ae") to "A",
+ Regex("ee") to "E",
+ Regex("ei") to "E",
+ Regex("ey") to "E",
+ Regex("ie") to "I",
+ Regex("ii") to "I",
+ Regex("iy") to "I",
+ Regex("ao") to "O",
+ Regex("au") to "O",
+ Regex("oo") to "O",
+ Regex("ou") to "U",
+ Regex("ue") to "U",
+ Regex("ui") to "U",
+ Regex("uu") to "U",
+ // Consonants
+ Regex("tz") to "C",
+ Regex("hh") to "K",
+ Regex("kh") to "K",
+ Regex("gh") to "G",
+ Regex("ng(?![aeiouAEIOU])") to "N",
+ Regex("ng([aeiouAEIOU])") to "Ng$1",
+ Regex("n'g") to "ng",
+ Regex("qh") to "Q",
+ Regex("th") to "T",
+
+ Regex("ck") to "q",
+ Regex("c") to "",
+ Regex("k") to "q",
+ )
+
+ fun pokhwalToFontAlphabet(pokhwal: String) = replacements.fold(pokhwal.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) ->
+ partial.replace(regex, replacement)
+ }
+}
+++ /dev/null
-package info.mechyrdia.lore
-
-import com.jaredrummler.fontreader.truetype.FontFileReader
-import com.jaredrummler.fontreader.truetype.TTFFile
-import com.jaredrummler.fontreader.util.GlyphSequence
-import info.mechyrdia.data.FileStorage
-import info.mechyrdia.data.StoragePath
-import info.mechyrdia.route.KeyedEnumSerializer
-import info.mechyrdia.yieldThread
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.awt.Font
-import java.awt.Shape
-import java.awt.geom.AffineTransform
-import java.awt.geom.GeneralPath
-import java.awt.geom.PathIterator
-import java.awt.image.BufferedImage
-import java.io.ByteArrayInputStream
-import java.io.IOException
-import java.nio.IntBuffer
-import kotlin.properties.ReadOnlyProperty
-
-@Serializable(with = TextAlignmentSerializer::class)
-enum class TextAlignment {
- LEFT {
- override fun processWidth(widthDiff: Int): Int {
- return 0
- }
- },
- CENTER {
- override fun processWidth(widthDiff: Int): Int {
- return widthDiff / 2
- }
- },
- RIGHT {
- override fun processWidth(widthDiff: Int): Int {
- return widthDiff
- }
- };
-
- abstract fun processWidth(widthDiff: Int): Int
-}
-
-object TextAlignmentSerializer : KeyedEnumSerializer<TextAlignment>(TextAlignment.entries)
-
-object MechyrdiaSansFont {
- private val logger: Logger = LoggerFactory.getLogger(MechyrdiaSansFont::class.java)
-
- suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): String {
- val (file, font) = getFont(bold, italic)
- return layoutText(text, file, font, align).toSvgDocument(80.0 / file.unitsPerEm, 12.0)
- }
-
- private val fontsRoot = StoragePath("fonts")
- private fun fontFile(name: String) = fontsRoot / "$name.ttf"
- private suspend fun loadFont(fontFile: StoragePath): Pair<TTFFile, Font>? {
- val bytes = FileStorage.instance.readFile(fontFile) ?: return null
-
- return withContext(Dispatchers.IO) {
- val file = TTFFile(true, true)
- file.readFont(FontFileReader(ByteArrayInputStream(bytes)))
-
- val font = Font
- .createFont(Font.TRUETYPE_FONT, ByteArrayInputStream(bytes))
- .deriveFont(file.unitsPerEm.toFloat())
-
- file to font
- }
- }
-
- private fun loadedFont(fontName: String): ReadOnlyProperty<Any?, suspend () -> Pair<TTFFile, Font>?> {
- return storedData(fontFile(fontName), ::loadFont)
- }
-
- private val mechyrdiaSans by loadedFont("mechyrdia-sans")
- private val mechyrdiaSansB by loadedFont("mechyrdia-sans-bold")
- private val mechyrdiaSansI by loadedFont("mechyrdia-sans-italic")
- private val mechyrdiaSansBI by loadedFont("mechyrdia-sans-bold-italic")
-
- private val mechyrdiaSansFonts = listOf(mechyrdiaSans, mechyrdiaSansI, mechyrdiaSansB, mechyrdiaSansBI)
- private suspend fun getFont(bold: Boolean, italic: Boolean): Pair<TTFFile, Font> {
- return mechyrdiaSansFonts[(if (bold) 2 else 0) + (if (italic) 1 else 0)]()!!
- }
-
- private fun TTFFile.getGlyph(cp: Int): Int {
- return try {
- unicodeToGlyph(cp)
- } catch (ex: IOException) {
- 0
- }
- }
-
- private fun String.toCodePointSequence() = sequence {
- val l = length
- var i = 0
- while (i < l) {
- val cp = Character.codePointAt(this@toCodePointSequence, i)
- i += if (Character.isSupplementaryCodePoint(cp)) 2 else 1
- yield(cp)
- }
- }
-
- private fun String.toCodePointArray(): IntArray {
- val iter = toCodePointSequence().iterator()
-
- return IntArray(codePointCount(0, length)) { _ ->
- assert(iter.hasNext())
- iter.next()
- }
- }
-
- private fun TTFFile.getGlyphs(str: String): GlyphSequence {
- val codes = str.toCodePointArray()
- val glyphs = IntArray(codes.size) { i -> getGlyph(codes[i]) }
-
- return GlyphSequence(IntBuffer.wrap(codes), IntBuffer.wrap(glyphs), null)
- }
-
- private fun TTFFile.getBasicWidths(glyphSequence: GlyphSequence): IntArray {
- return IntArray(glyphSequence.glyphCount) { i ->
- if (i == 0)
- mtx[glyphSequence.getGlyph(i)].wx
- else {
- val prev = glyphSequence.getGlyph(i - 1)
- val curr = glyphSequence.getGlyph(i)
- (rawKerning[prev]?.get(curr) ?: 0) + mtx[curr].wx
- }
- }
- }
-
- private fun TTFFile.getGlyphPositions(glyphSequence: GlyphSequence, widths: IntArray): Array<IntArray> {
- val adjustments = Array(glyphSequence.glyphCount) { IntArray(4) }
- gpos.position(glyphSequence, "latn", "*", 0, widths, adjustments)
-
- // I don't know why this is necessary,
- // but it gives me the results I want.
- for (adjustment in adjustments) {
- adjustment[0] *= 2
- adjustment[1] *= 2
- adjustment[2] *= 2
- adjustment[3] *= 2
- }
-
- return adjustments
- }
-
- private fun getWidth(widths: IntArray, glyphPositions: Array<IntArray>): Int {
- return widths.zip(glyphPositions) { width, pos -> width + pos[2] }.sum()
- }
-
- private fun layoutText(text: String, file: TTFFile, font: Font, align: TextAlignment): Shape {
- val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB)
- val g2d = img.createGraphics()
- try {
- val charHolder = CharArray(2)
- val lineHeight = file.rawLowerCaseAscent - file.rawLowerCaseDescent
-
- val lines = text.split("\r\n", "\n", "\r")
- val lineGlyphs = lines.map { file.getGlyphs(it) }
- val lineBasics = lineGlyphs.map { file.getBasicWidths(it) }
- val lineAdjust = lineGlyphs.zip(lineBasics) { glyphs, widths -> file.getGlyphPositions(glyphs, widths) }
- val lineWidths = lineBasics.zip(lineAdjust) { width, adjust -> getWidth(width, adjust) }
- val blockWidth = lineWidths.max()
- var ly = 0
-
- yieldThread()
-
- val shape = GeneralPath()
- val tf = AffineTransform()
- for ((li, line) in lines.withIndex()) {
- if (line.isNotBlank()) {
- val lineWidth = lineWidths[li]
- val lx = align.processWidth(blockWidth - lineWidth)
-
- var cx = 0
- var cy = 0
-
- val basicAdv = lineBasics[li]
- val adjusted = lineAdjust[li]
- val glyphSeq = lineGlyphs[li]
- for ((ci, codePoint) in glyphSeq.getCharacterArray(false).withIndex()) {
- val length = Character.toChars(codePoint, charHolder, 0)
- val glyph = font.layoutGlyphVector(g2d.fontRenderContext, charHolder, 0, length, Font.LAYOUT_LEFT_TO_RIGHT)
- val glyphShape = glyph.outline as GeneralPath
- val glyphShift = adjusted[ci]
-
- tf.setToTranslation((lx + cx + glyphShift[0]).toDouble(), (ly + cy + glyphShift[1]).toDouble())
- shape.append(glyphShape.getPathIterator(tf), false)
-
- cx += glyphShift[2] + basicAdv[ci]
- cy += glyphShift[3]
- }
- }
-
- ly += lineHeight
-
- yieldThread()
- }
-
- return shape
- } catch (ex: Exception) {
- logger.error("Error converting text $text to font shape", ex)
- return GeneralPath()
- } finally {
- g2d.dispose()
- }
- }
-
- private fun Shape.toSvgDocument(scale: Double, padding: Double = 0.0): String {
- return buildString {
- appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
-
- val viewBox = bounds2D
- val vBoxPad = padding / scale
- val sizePad = padding * 2
-
- appendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${(viewBox.width * scale) + sizePad}\" height=\"${(viewBox.height * scale) + sizePad}\" viewBox=\"${viewBox.minX - vBoxPad} ${viewBox.minY - vBoxPad} ${viewBox.width + (vBoxPad * 2)} ${viewBox.height + (vBoxPad * 2)}\">")
- appendLine(toSvgPath())
- appendLine("</svg>")
- }
- }
-
- private fun Shape.toSvgPath(): String {
- return buildString {
- append("<path d=\"")
-
- val iterator = getPathIterator(null)
- val coords = DoubleArray(6)
- var isFirst = true
- while (!iterator.isDone) {
- if (isFirst)
- isFirst = false
- else
- append(' ')
-
- when (val segment = iterator.currentSegment(coords)) {
- PathIterator.SEG_MOVETO -> {
- append("M ${coords[0]},${coords[1]}")
- }
-
- PathIterator.SEG_LINETO -> {
- append("L ${coords[0]},${coords[1]}")
- }
-
- PathIterator.SEG_QUADTO -> {
- append("Q ${coords[0]},${coords[1]} ${coords[2]},${coords[3]}")
- }
-
- PathIterator.SEG_CUBICTO -> {
- append("C ${coords[0]},${coords[1]} ${coords[2]},${coords[3]} ${coords[4]},${coords[5]}")
- }
-
- PathIterator.SEG_CLOSE -> {
- append("Z")
- }
-
- else -> error("Invalid segment type $segment")
- }
-
- iterator.next()
-
- yieldThread()
- }
-
- append("\" fill-rule=\"")
- when (val winding = iterator.windingRule) {
- PathIterator.WIND_EVEN_ODD -> append("evenodd")
- PathIterator.WIND_NON_ZERO -> append("nonzero")
- else -> error("Invalid winding rule $winding")
- }
-
- append("\" />")
- }
- }
-}
-
-object TylanAlphabetFont {
- private val allowedTranslitCharacters = setOf(
- ' ', '\r', '\n', '\t',
- 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y',
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
- '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+',
- '[', '{', '}', ']', '\\', '|', '/', '<',
- '.', ',', ':', ';', '\'', '"', '?', '>',
- )
-
- private val replacements = listOf(
- Regex("([0-9xy]+)(?![\\s0-9xy])") to "$1 ",
- Regex("(?<![\\s0-9xy])([0-9xy]+)") to " $1",
-
- Regex("([a-pr-v'])m(?![\\w'])") to "$1A",
- Regex("([a-pr-v'])n(?![\\w'])") to "$1E",
- Regex("([a-ps-v'])r(?![\\w'])") to "$1H",
- Regex("([ac-fh-jl-or-v'])s(?![\\w'])") to "$1I",
- Regex("([a-pr-v'])t(?![\\w'])") to "$1Q",
-
- Regex("x") to "M",
- Regex("y") to "N",
- Regex("[qw-z]") to "",
-
- Regex("ch") to "C",
- Regex("dh") to "D",
- Regex("sh") to "S",
- Regex("th") to "T",
-
- Regex("bs") to "B",
- Regex("gs") to "G",
- Regex("ks") to "K",
- Regex("ps") to "P",
-
- Regex("b'([Is])") to "b$1",
- Regex("g'([Is])") to "g$1",
- Regex("k'([Is])") to "k$1",
- Regex("p'([Is])") to "p$1",
-
- Regex("ff") to "F",
- Regex("fv") to "V",
- Regex("hj") to "J",
- Regex("lh") to "L",
- Regex("ll") to "L",
- Regex("hl") to "L",
- Regex("rh") to "R",
- Regex("rr") to "R",
- Regex("hr") to "R",
- Regex("d'h") to "dh",
- Regex("l'h") to "lh",
- Regex("l'J") to "lJ",
- Regex("r'h") to "rh",
- Regex("r'J") to "rJ",
- Regex("s'h") to "sh",
- Regex("t'h") to "th",
- Regex("h'l") to "hl",
- Regex("h'r") to "hr",
- Regex("B'h") to "Bh",
- Regex("G'h") to "Gh",
- Regex("K'h") to "Kh",
- Regex("P'h") to "Ph",
- Regex("c(?!h)") to "",
-
- Regex("ae") to "W",
- Regex("au") to "X",
- Regex("ea") to "Y",
- Regex("ei") to "Z",
- Regex("eo") to "w",
- Regex("eu") to "x",
- Regex("oa") to "y",
- Regex("ou") to "z",
- Regex("oe") to "O",
- Regex("ui") to "U",
-
- Regex("i([aeiouOUw-zW-Z])") to "ij$1",
- Regex("^([aeiouOUw-zW-Z])") to "'$1",
- Regex("([^'BCDFGJKLPR-TVbdf-hj-npr-tv])([aeiouOUw-zW-Z])") to "$1'$2",
- Regex("([aeiouOUw-zW-Z])([aeiouOUw-zW-Z])") to "$1'$2",
- )
-
- fun tylanToFontAlphabet(tylan: String) = replacements.fold(tylan.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) ->
- partial.replace(regex, replacement)
- }
-}
-
-object PokhwalishAlphabetFont {
- private val allowedTranslitCharacters = setOf(
- ' ', '\r', '\n', '\t',
- 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'y', 'z',
- '.', ',', '\'', '?', '!',
- )
-
- private val replacements = listOf(
- // Vowels
- Regex("aa") to "A",
- Regex("ae") to "A",
- Regex("ee") to "E",
- Regex("ei") to "E",
- Regex("ey") to "E",
- Regex("ie") to "I",
- Regex("ii") to "I",
- Regex("iy") to "I",
- Regex("ao") to "O",
- Regex("au") to "O",
- Regex("oo") to "O",
- Regex("ou") to "U",
- Regex("ue") to "U",
- Regex("ui") to "U",
- Regex("uu") to "U",
- // Consonants
- Regex("tz") to "C",
- Regex("hh") to "K",
- Regex("kh") to "K",
- Regex("gh") to "G",
- Regex("ng(?![aeiouAEIOU])") to "N",
- Regex("ng([aeiouAEIOU])") to "Ng$1",
- Regex("n'g") to "ng",
- Regex("qh") to "Q",
- Regex("th") to "T",
-
- Regex("ck") to "q",
- Regex("c") to "",
- Regex("k") to "q",
- )
-
- fun pokhwalToFontAlphabet(pokhwal: String) = replacements.fold(pokhwal.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) ->
- partial.replace(regex, replacement)
- }
-}