import com.jaredrummler.fontreader.truetype.FontFileReader
import com.jaredrummler.fontreader.truetype.TTFFile
import com.jaredrummler.fontreader.util.GlyphSequence
-import info.mechyrdia.concat
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
-import info.mechyrdia.data.XmlTagConsumer
-import info.mechyrdia.data.declaration
-import info.mechyrdia.data.root
import info.mechyrdia.route.KeyedEnumSerializer
import info.mechyrdia.yieldThread
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.Serializable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+import java.awt.AlphaComposite
+import java.awt.Color
import java.awt.Font
-import java.awt.Shape
+import java.awt.RenderingHints
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.ByteArrayOutputStream
import java.io.IOException
import java.nio.IntBuffer
+import javax.imageio.ImageIO
+import kotlin.math.ceil
+import kotlin.math.roundToInt
import kotlin.properties.ReadOnlyProperty
private val FontsLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.lore.FontsKt")
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)
- .root(
- "svg",
- namespace = "http://www.w3.org/2000/svg",
- attributes = mapOf(
- "width" to svgDoc.width.xmlValue,
- "height" to svgDoc.height.xmlValue,
- "viewBox" to listOf(
- svgDoc.viewBoxX,
- svgDoc.viewBoxY,
- svgDoc.viewBoxW,
- svgDoc.viewBoxH,
- ).concat(" ") { it.xmlValue }
- )
- ) { "path"(attributes = mapOf("d" to svgDoc.path.d, "fill-rule" to svgDoc.path.fillRule)) }
-
object MechyrdiaSansFont {
- suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): SvgDoc {
+ suspend fun renderTextToPng(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): ByteArray {
val (file, font) = getFont(bold, italic)
return runInterruptible(Dispatchers.Default) {
val shape = layoutText(text, file, font, align)
- createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0)
+ createPngImage(shape, 160.0 / file.unitsPerEm, 24.0)
}
}
return widths.zip(glyphPositions) { width, pos -> width + pos[2] }.sum()
}
- private fun layoutText(text: String, file: TTFFile, font: Font, align: TextAlignment): Shape {
+ private fun layoutText(text: String, file: TTFFile, font: Font, align: TextAlignment): GeneralPath {
val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB)
val g2d = img.createGraphics()
try {
}
}
- private fun createSvgDocument(shape: Shape, scale: Double, padding: Double = 0.0): SvgDoc {
+ private fun createPngImage(shape: GeneralPath, scale: Double, padding: Double = 0.0): ByteArray {
+ shape.transform(AffineTransform.getScaleInstance(scale, scale))
+
val viewBox = shape.bounds2D
- val vBoxPad = padding / scale
- val sizePad = padding * 2
+ val imageWidth = ceil(viewBox.width + padding * 2).roundToInt()
+ val imageHeight = ceil(viewBox.height + padding * 2).roundToInt()
- val path = shape.calculateSvgPath()
+ shape.transform(AffineTransform.getTranslateInstance(padding - viewBox.minX, padding - viewBox.minY))
- 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
+ val image = BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB)
+
+ val g2d = image.createGraphics()
+ AutoCloseable(g2d::dispose).use { _ ->
+ g2d.color = Color(0xFF, 0xFF, 0xFF, 0x00)
+ g2d.composite = AlphaComposite.Src
+ g2d.fillRect(0, 0, imageWidth, imageHeight)
- 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()
- }
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
+
+ g2d.color = Color(0x00, 0x00, 0x00, 0xFF)
+ g2d.composite = AlphaComposite.SrcOver
+ g2d.fill(shape)
}
- val fillRule = when (val winding = iterator.windingRule) {
- PathIterator.WIND_EVEN_ODD -> "evenodd"
- PathIterator.WIND_NON_ZERO -> "nonzero"
- else -> error("Invalid winding rule $winding")
- }
+ val output = ByteArrayOutputStream()
+ ImageIO.write(image, "PNG", output)
- return SvgPath(d, fillRule)
+ return output.toByteArray()
}
}
import info.mechyrdia.lore.respondAsset
import info.mechyrdia.lore.respondRss
import info.mechyrdia.lore.sitemap
-import info.mechyrdia.lore.svg
import info.mechyrdia.lore.toCommentHtml
import info.mechyrdia.lore.toFragmentString
import info.mechyrdia.robot.RobotService
import io.ktor.server.html.respondHtml
import io.ktor.server.plugins.MissingRequestParameterException
import io.ktor.server.response.header
+import io.ktor.server.response.respondBytes
import io.ktor.server.response.respondText
import io.ktor.server.response.respondTextWriter
import io.ktor.server.routing.RoutingContext
override suspend fun RoutingContext.handleCall(payload: MechyrdiaSansPayload) {
with(utils) { call.filterCall() }
- val svgDoc = MechyrdiaSansFont.renderTextToSvg(payload.lines.concat("\n") { it.trim() }, payload.bold, payload.italic, payload.align)
- call.respondXml(contentType = ContentType.Image.SVG) {
- svg(svgDoc)
- }
+ val pngBytes = MechyrdiaSansFont.renderTextToPng(payload.lines.concat("\n") { it.trim() }, payload.bold, payload.italic, payload.align)
+ call.respondBytes(pngBytes, contentType = ContentType.Image.PNG)
}
}
function appendWithLineBreaks(element, lines) {
let isFirst = true;
for (const line of lines) {
- if (isFirst)
+ if (isFirst) {
isFirst = false;
- else
+ } else {
element.append(document.createElement("br"));
+ }
element.append(line);
}
}
(function () {
// Mechyrdian font
+ const blank1x1Png = new Uint8Array([
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
+ 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xff, 0xff, 0x3f,
+ 0x03, 0x00, 0x08, 0xfc, 0x02, 0xfe, 0xa7, 0x9a, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
+ 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
+ ]).buffer;
+
/**
* @param {HTMLInputElement} input
* @param {HTMLInputElement} boldOpt
let outBlob;
if (inText.trim().length === 0) {
- outBlob = new Blob([
- "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
- "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"0\">\n",
- "</svg>\n"
- ], {type: "image/svg+xml"});
+ outBlob = new Blob([blank1x1Png], {type: "image/png"});
} else {
const urlParams = new URLSearchParams();
if (boldOpt.checked) urlParams.set("bold", "true");
if (italicOpt.checked) urlParams.set("italic", "true");
urlParams.set("align", alignOpt.value);
- for (const line of inText.split("\n"))
+ for (const line of inText.split("\n")) {
urlParams.append("lines", line.trim());
+ }
outBlob = await (await fetch("/utils/mechyrdia-sans", {
method: "POST",
}
const prevObjectUrl = output.src;
- if (prevObjectUrl != null && prevObjectUrl.length > 0)
+ if (prevObjectUrl != null && prevObjectUrl.length > 0) {
URL.revokeObjectURL(prevObjectUrl);
+ }
output.src = URL.createObjectURL(outBlob);
}
const inText = input.value;
const urlParams = new URLSearchParams();
- for (const line of inText.split("\n"))
+ for (const line of inText.split("\n")) {
urlParams.append("lines", line.trim());
+ }
const outText = await (await fetch("/utils/tylan-lang", {
method: "POST",
body: urlParams,
})).text();
- if (inText === input.value)
+ if (inText === input.value) {
output.value = outText;
+ }
}
const tylanAlphabetBoxes = dom.querySelectorAll("div.tylan-alphabet-box");
const inText = input.value;
const urlParams = new URLSearchParams();
- for (const line of inText.split("\n"))
+ for (const line of inText.split("\n")) {
urlParams.append("lines", line.trim());
+ }
const outText = await (await fetch("/utils/pokhwal-lang", {
method: "POST",
body: urlParams,
})).text();
- if (inText === input.value)
+ if (inText === input.value) {
output.value = outText;
+ }
}
const pokhwalAlphabetBoxes = dom.querySelectorAll("div.pokhwal-alphabet-box");
const inText = input.value;
await delay(500);
- if (input.value !== inText)
+ if (input.value !== inText) {
return;
+ }
if (inText.length === 0) {
output.innerHTML = "";
}
const urlParams = new URLSearchParams();
- for (const line of inText.split("\n"))
+ for (const line of inText.split("\n")) {
urlParams.append("lines", line.trim());
+ }
const outText = await (await fetch("/utils/preview-comment", {
method: "POST",
},
body: urlParams,
})).text();
- if (input.value !== inText)
+ if (input.value !== inText) {
return;
+ }
output.innerHTML = "<h3>Preview:</h3>" + outText;
}
e.preventDefault();
const thisElement = e.currentTarget;
- if (thisElement.hasAttribute("data-copying"))
+ if (thisElement.hasAttribute("data-copying")) {
return;
+ }
const elementHtml = thisElement.innerHTML;
const redirectIdValue = window.location.hash;
const redirectIds = dom.querySelectorAll("h1[data-redirect-id], h2[data-redirect-id], h3[data-redirect-id], h4[data-redirect-id], h5[data-redirect-id], h6[data-redirect-id]");
for (const redirectId of redirectIds) {
- if (redirectId.getAttribute("data-redirect-id") !== redirectIdValue)
+ if (redirectId.getAttribute("data-redirect-id") !== redirectIdValue) {
continue;
+ }
const pElement = document.createElement("p");
pElement.style.fontSize = "0.8em";