indigoplugin.generators.FontGen.scala Maven / Gradle / Ivy
The newest version!
package indigoplugin.generators
import indigoplugin.FontOptions
import scala.annotation.tailrec
/** Provides functionality for generating font images and associated FontInfo instances.
*/
object FontGen {
def generate(
moduleName: String,
fullyQualifiedPackage: String,
fontFilePath: os.Path,
fontOptions: FontOptions,
imageOut: os.Path
): os.Path => Seq[os.Path] = outDir => {
// Some director sanity checking...
if (!os.exists(imageOut)) {
throw new Exception(
s"The supplied path to the output directory for the font-sheet image does not exist: ${imageOut.toString}"
)
} else if (!os.isDir(imageOut)) {
throw new Exception(
s"The supplied path to the output directory for the font-sheet image is not a directory: ${imageOut.toString}"
)
} else {
//
}
val wd = outDir / Generators.OutputDirName
os.makeDir.all(wd)
val file = wd / s"$moduleName.scala"
val helper = FontAWTHelper.makeHelper(fontFilePath.toIO, fontOptions.fontSize)
// Process into laid out details
val initialCharDetails =
fontOptions.charSet.toCharacterCodes.map { c =>
val (w, h, a) = helper.getCharBounds(c.toChar)
CharDetail(c.toChar, c, 0, 0, w, h, a)
}.toList
val filteredCharDetails =
initialCharDetails
.filter(cd => filterUnsupportedChars(cd.char, cd.code))
// if (initialCharDetails.length > filteredCharDetails.length)
// println("WARNING: Some unsupported characters were filtered out.")
val charDetails =
layout(
filteredCharDetails,
helper.getBaselineOffset,
fontOptions.maxCharactersPerLine
)
// Write out FontInfo
val default =
charDetails
.find(_.char == fontOptions.charSet.default)
.getOrElse(throw new Exception(s"Couldn't find default character '${fontOptions.charSet.default.toString}'"))
val (sheetWidth, sheetHeight) = findBounds(charDetails)
val fontInfo =
genFontInfo(moduleName, fullyQualifiedPackage, fontOptions.fontKey, sheetWidth, sheetHeight, default, charDetails)
os.write.over(file, fontInfo)
// Write out font image
val outImageFileName = s"$moduleName.png"
helper.drawFontSheet((imageOut / outImageFileName).toIO, charDetails, sheetWidth, sheetHeight, fontOptions)
Seq(file)
}
def sanitiseName(name: String, ext: String): String = {
val noExt = if (ext.nonEmpty && name.endsWith(ext)) name.dropRight(ext.length) else name
noExt.replaceAll("[^A-Za-z0-9]", "-").split("-").map(_.capitalize).mkString
}
// TODO: Does nothing
def layout(unplacedChars: List[CharDetail], lineSpacing: Int, maxCharsPerLine: Int): List[CharDetail] = {
@tailrec
def rec(
remaining: List[CharDetail],
lineCount: Int,
charCount: Int,
nextX: Int,
acc: List[CharDetail]
): List[CharDetail] =
remaining match {
case Nil =>
acc.reverse
case c :: cs if charCount == maxCharsPerLine =>
rec(c :: cs, lineCount + 1, 0, 0, acc)
case c :: cs =>
val x = nextX
val y = lineCount * lineSpacing
val newC = c.copy(x = x, y = y)
rec(cs, lineCount, charCount + 1, nextX + c.width, newC :: acc)
}
rec(unplacedChars, 0, 0, 0, Nil)
}
def findBounds(charDetails: List[CharDetail]): (Int, Int) =
charDetails.foldLeft((0, 0)) { case ((w, h), c) =>
(
if (c.x + c.width > w) c.x + c.width else w,
if (c.y + c.height > h) c.y + c.height else h
)
}
def genFontInfo(
moduleName: String,
fullyQualifiedPackage: String,
name: String,
sheetWidth: Int,
sheetHeight: Int,
default: CharDetail,
chars: List[CharDetail]
): String = {
val charString = chars
.map(cd => " " + toFontChar(cd) + ",")
.mkString("\n")
.dropRight(1) // Drops the last ','
val dx = default.x.toString
val dy = default.y.toString
val dw = default.width.toString
val dh = default.height.toString
s"""package $fullyQualifiedPackage
|
|import indigo.*
|
|// DO NOT EDIT: Generated by Indigo.
|object $moduleName {
|
| val fontKey: FontKey = FontKey("$name")
|
| val fontInfo: FontInfo =
| FontInfo(
| fontKey,
| $sheetWidth,
| $sheetHeight,
| FontChar("${default.char.toString()}", $dx, $dy, $dw, $dh)
| ).isCaseSensitive
| .addChars(
| Batch(
|$charString
| )
| )
|
|}
|""".stripMargin
}
def toFontChar(
charDetail: CharDetail
): String = {
val c = escapeChar(charDetail.char)
val x = charDetail.x.toString()
val y = charDetail.y.toString()
val w = charDetail.width.toString()
val h = charDetail.height.toString()
s"""FontChar("$c", $x, $y, $w, $h)"""
}
def filterUnsupportedChars(c: Char, code: Int): Boolean =
c match {
case '\n' => false
case '\t' => false
case '\b' => false
case '\r' => false
case '\f' => false
case _ if code == 0 => false
case _ if code == 26 => false
case _ => true
}
def escapeChar(c: Char): String =
c match {
case '\\' => "\\\\"
case '\"' => "\\\""
case '\'' => "\\'"
case _ => c.toString
}
def charCodesToRanges(charCodes: List[Int]): List[FromTo] = {
val codes = charCodes.distinct.sorted
codes.headOption match {
case None =>
List.empty[FromTo]
case Some(start) =>
codes.tail.foldLeft(List(FromTo(start))) { case (acc, code) =>
acc.headOption match {
case Some(FromTo(from, to)) if code == to + 1 => FromTo(from, code) :: acc.tail
case _ => FromTo(code) :: acc
}
}
}
}
final case class FromTo(from: Int, to: Int)
object FromTo {
def apply(code: Int): FromTo =
FromTo(code, code)
}
}
object FontAWTHelper {
import java.awt._
import java.awt.image._
import java.io._
import javax.imageio.ImageIO
def makeHelper(fontFile: File, fontSize: Int): Helper = {
val font =
Font
.createFont(Font.TRUETYPE_FONT, fontFile)
.deriveFont(fontSize.toFloat)
Helper(font)
}
final case class Helper(font: Font) {
def getCharBounds(char: Char): (Int, Int, Int) = {
val tmpBuffer = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
val tmpG2d = tmpBuffer.createGraphics()
tmpG2d.setFont(font)
val fontMetrics = tmpG2d.getFontMetrics()
val w = fontMetrics.charWidth(char)
val h = fontMetrics.getHeight()
val a = fontMetrics.getAscent()
tmpG2d.dispose()
(w, h, a)
}
def getBaselineOffset: Int = {
val tmpBuffer = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
val tmpG2d = tmpBuffer.createGraphics()
tmpG2d.setFont(font)
val fontMetrics = tmpG2d.getFontMetrics()
fontMetrics.getLeading() + fontMetrics.getAscent() + fontMetrics.getDescent()
}
def drawFontSheet(
outFile: File,
charDetails: scala.collection.immutable.List[CharDetail],
sheetWidth: Int,
sheetHeight: Int,
fontOptions: FontOptions
): Unit = {
val bufferedImage = new BufferedImage(sheetWidth, sheetHeight, BufferedImage.TYPE_INT_ARGB)
val g2d = bufferedImage.createGraphics()
if (fontOptions.antiAlias) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
}
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
g2d.setFont(font)
g2d.setColor(fontOptions.color.toColor)
charDetails.foreach { c =>
g2d.drawString(c.char.toString, c.x, c.y + c.ascent)
}
g2d.dispose()
ImageIO.write(bufferedImage, "PNG", outFile)
()
}
}
}
final case class CharDetail(char: Char, code: Int, x: Int, y: Int, width: Int, height: Int, ascent: Int)