All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy