import io.ktor.server.sessions.*
import io.ktor.server.sessions.serialization.*
import io.ktor.util.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
+import kotlinx.coroutines.withContext
import org.slf4j.event.Level
import java.io.File
import java.io.IOException
// Utilities
+ post("/mechyrdia-sans") {
+ val queryString = call.request.queryParameters
+
+ val isBold = "true".equals(queryString["bold"], ignoreCase = true)
+ val isItalic = "true".equals(queryString["italic"], ignoreCase = true)
+
+ val alignArg = queryString["align"]
+ val align = MechyrdiaSansFont.Alignment.entries.singleOrNull {
+ it.name.equals(alignArg, ignoreCase = true)
+ } ?: MechyrdiaSansFont.Alignment.LEFT
+
+ val text = call.receiveText()
+ val svg = runInterruptible(Dispatchers.Default) {
+ MechyrdiaSansFont.renderTextToSvg(text, isBold, isItalic, align)
+ }
+
+ call.respondText(svg, ContentType.Image.SVG)
+ }
+
post("/tylan-lang") {
- call.respondText(TylanAlphabet.tylanToFontAlphabet(call.receiveText()))
+ call.respondText(TylanAlphabetFont.tylanToFontAlphabet(call.receiveText()))
}
post("/pokhwal-lang") {
- call.respondText(PokhwalishAlphabet.pokhwalToFontAlphabet(call.receiveText()))
+ call.respondText(PokhwalishAlphabetFont.pokhwalToFontAlphabet(call.receiveText()))
}
post("/preview-comment") {
td {
+"Writes text in the Tylan alphabet: "
span(classes = "lang-tylan") {
- +TylanAlphabet.tylanToFontAlphabet("rheagda tulasra")
+ +TylanAlphabetFont.tylanToFontAlphabet("rheagda tulasra")
}
}
}
td {
+"Writes text in the Pokhwalish alphabet: "
span(classes = "lang-pokhwal") {
- +PokhwalishAlphabet.pokhwalToFontAlphabet("pokhvalsqo jargo")
+ +PokhwalishAlphabetFont.pokhwalToFontAlphabet("pokhvalsqo jargo")
}
}
}
--- /dev/null
+package info.mechyrdia.lore
+
+import java.awt.Font
+import java.awt.RenderingHints
+import java.awt.Shape
+import java.awt.geom.AffineTransform
+import java.awt.geom.GeneralPath
+import java.awt.geom.PathIterator
+import java.awt.image.BufferedImage
+
+object MechyrdiaSansFont {
+ enum class Alignment(val amount: Double) {
+ LEFT(0.0), CENTER(0.5), RIGHT(1.0),
+ }
+
+ fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: Alignment, standalone: Boolean = true): String {
+ val font = if (bold) {
+ if (italic)
+ mechyrdiaSansBI
+ else
+ mechyrdiaSansB
+ } else
+ if (italic)
+ mechyrdiaSansI
+ else
+ mechyrdiaSans
+
+ val shape = layoutText(text, font, align.amount)
+ return shape.toSvgDocument(standalone)
+ }
+
+ private const val DEFAULT_FONT_SIZE = 48f
+
+ private fun loadFont(font: String): Font {
+ return Font
+ .createFont(Font.TRUETYPE_FONT, javaClass.getResourceAsStream("/fonts/$font.ttf")!!)
+ .deriveFont(DEFAULT_FONT_SIZE)
+ }
+
+ private val mechyrdiaSans = loadFont("mechyrdia-sans")
+ private val mechyrdiaSansB = loadFont("mechyrdia-sans-bold")
+ private val mechyrdiaSansI = loadFont("mechyrdia-sans-italic")
+ private val mechyrdiaSansBI = loadFont("mechyrdia-sans-bold-italic")
+
+ private fun layoutText(text: String, font: Font, alignAmount: Double): Shape {
+ val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB)
+ val g2d = img.createGraphics()
+ try {
+ g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
+
+ val fontMetrics = g2d.getFontMetrics(font)
+ val lines = text.split("\r\n", "\n", "\r")
+ val width = lines.maxOf { fontMetrics.stringWidth(it) }.toDouble()
+ var y = 0.0
+
+ val shape = GeneralPath()
+ val tf = AffineTransform()
+ for (line in lines) {
+ if (line.isNotBlank()) {
+ val x = (width - fontMetrics.stringWidth(line)) * alignAmount
+
+ val glyphs = font.layoutGlyphVector(g2d.fontRenderContext, line.toCharArray(), 0, line.length, Font.LAYOUT_LEFT_TO_RIGHT)
+ val textShape = glyphs.outline as GeneralPath
+
+ tf.setToIdentity()
+ tf.translate(x, y)
+ shape.append(textShape.getPathIterator(tf), false)
+ }
+
+ y += fontMetrics.height
+ }
+
+ return shape
+ } finally {
+ g2d.dispose()
+ }
+ }
+
+ private fun Shape.toSvgDocument(standalone: Boolean = true): String {
+ return buildString {
+ if (standalone)
+ appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>")
+
+ val viewBox = bounds2D
+ appendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"${viewBox.width * 2}\" height=\"${viewBox.height * 2}\" viewBox=\"${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}\">")
+ appendLine(toSvgPath())
+ appendLine("</svg>")
+ }
+ }
+
+ private fun Shape.toSvgPath(): String {
+ return buildString {
+ append("<path d=\"")
+
+ val iterator = getPathIterator(null)
+ val coords = DoubleArray(6)
+ while (!iterator.isDone) {
+ 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()
+ }
+
+ 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)
+ }
+}
+++ /dev/null
-package info.mechyrdia.lore
-
-object TylanAlphabet {
- 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 PokhwalishAlphabet {
- 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)
- }
-}
TextParserTagType.Indirect { tagParam, content, _ ->
if (tagParam?.equals("tylan", ignoreCase = true) == true) {
val uncensored = TextParserState.uncensorText(content)
- val tylan = TylanAlphabet.tylanToFontAlphabet(uncensored)
+ val tylan = TylanAlphabetFont.tylanToFontAlphabet(uncensored)
val recensored = TextParserState.censorText(tylan)
"<span class=\"lang-tylan\">$recensored</span>"
} else if (tagParam?.equals("thedish", ignoreCase = true) == true)
"<span class=\"lang-kishari\">$content</span>"
else if (tagParam?.equals("pokhval", ignoreCase = true) == true || tagParam?.equals("pokhwal", ignoreCase = true) == true) {
val uncensored = TextParserState.uncensorText(content)
- val pokhwal = PokhwalishAlphabet.pokhwalToFontAlphabet(uncensored)
+ val pokhwal = PokhwalishAlphabetFont.pokhwalToFontAlphabet(uncensored)
val recensored = TextParserState.censorText(pokhwal)
"<span class=\"lang-pokhwal\">$recensored</span>"
} else content
),
ALPHABET(
TextParserTagType.Indirect { _, content, _ ->
- if (content.equals("tylan", ignoreCase = true)) {
+ if (content.equals("mechyrdian", ignoreCase = true)) {
+ """
+ |<div class="mechyrdia-sans-box">
+ |<p>Input Text:</p>
+ |<textarea class="input-box" spellcheck="false"></textarea>
+ |<p>Font options:</p>
+ |<ul>
+ |<li><label><input type="checkbox" class="bold-option"/> Bold</label></li>
+ |<li><label><input type="checkbox" class="ital-option"/> Italic</label></li>
+ |<li><label>Align: <select class="align-opts"><option value="left" selected>Left</option><option value="center">Center</option><option value="right">Right</option></select></label></li>
+ |</ul>
+ |<p>Rendered in Mechyrdia Sans:</p>
+ |<img class="output-img" style="display:block;max-width:100%"/>
+ |</div>
+ """.trimMargin()
+ } else if (content.equals("tylan", ignoreCase = true)) {
"""
|<div class="tylan-alphabet-box">
|<p>Latin Alphabet:</p>
});
}
+ window.addEventListener("load", function () {
+ // Mechyrdian font
+ async function mechyrdianToFont(input, boldOpt, italicOpt, alignOpt, output) {
+ const inText = input.value;
+
+ await delay(1500);
+ if (inText !== input.value) return;
+
+ let queryString = "?";
+ queryString += boldOpt.checked ? "bold=true&" : "";
+ queryString += italicOpt.checked ? "italic=true&" : "";
+ queryString += "align=" + alignOpt.value;
+
+ const outBlob = await (await fetch('/mechyrdia-sans' + queryString, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ body: inText,
+ })).blob();
+
+ if (inText !== input.value) return;
+
+ const prevObjectUrl = output.src;
+ if (prevObjectUrl != null && prevObjectUrl.length > 0)
+ URL.revokeObjectURL(prevObjectUrl);
+
+ output.src = URL.createObjectURL(outBlob);
+ }
+
+ const mechyrdiaSansBoxes = document.getElementsByClassName("mechyrdia-sans-box");
+ for (const mechyrdiaSansBox of mechyrdiaSansBoxes) {
+ const inputBox = mechyrdiaSansBox.getElementsByClassName("input-box")[0];
+ const boldOpt = mechyrdiaSansBox.getElementsByClassName("bold-option")[0];
+ const italicOpt = mechyrdiaSansBox.getElementsByClassName("ital-option")[0];
+ const alignOpt = mechyrdiaSansBox.getElementsByClassName("align-opts")[0];
+ const outputBox = mechyrdiaSansBox.getElementsByClassName("output-img")[0];
+
+ const inputListener = () => mechyrdianToFont(inputBox, boldOpt, italicOpt, alignOpt, outputBox);
+ boldOpt.addEventListener("change", inputListener);
+ italicOpt.addEventListener("change", inputListener);
+ alignOpt.addEventListener("change", inputListener);
+ inputBox.addEventListener("input", inputListener);
+ }
+ });
+
window.addEventListener("load", function () {
// Tylan alphabet
async function tylanToFont(input, output) {