Placeholder Image Generation with Scala

(, en)

Using placeholder images in you tests is a nice way to test different conditions without adding images for every test condition to your repo. You can of course use one of the free services, such as via.placeholder.com or dummyimage.com, but the two big elephants in the room are security and reliability. Do you actually know how secure this service is? Will it be subject to hacking attempts? What if they send you garbage or malicious images? Equally important is the question if you want to rely on a service that does not give any guarantees regarding uptime, responsiveness or responsibilities?

Well, how difficult can it be to create a simple placeholder generator in Scala (or eventually Java, because we are using the java.awt.Graphics2D class.

The objective is quite simple: create a rectangle in grey and generate some example on top of it. Something like this:

Placeholder

This question has been already answered more or less in StackOverflow post titled Convert Text Content to Image in Java, but if you want to generate placeholders, you need some extras.

Setting the stage

I use 2 helper classes for dimensions positions

// src/main/scala/Placeholder.scala
case class Dimensions(width: Int, height: Int)
case class Position(x: Int, y: Int)

That helps to keep things organised.

Selecting the font size

Of course you want to have the text fit the placeholder, so you need to figure out what font size you can use. I predefined some common font sizes and iterate through them to find the largest one that still fits.

// src/main/scala/Placeholder.scala
val fontSizes = List(6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)

def selectFont(
    imgDims: Dimensions,
    fontSizes: List[Int],
    text: String
): Option[(Font, Dimensions, Int)] = {
  val img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
  val g2d = img.createGraphics
  val res =
    try fontSizes.reverse.view
      .map { size =>
        val font = new Font("SansSerif", Font.PLAIN, size)
        g2d.setFont(font)
        val fm = g2d.getFontMetrics

        (font, Dimensions(fm.stringWidth(text), fm.getHeight), fm.getAscent)
      }
      .find { case (_, fDims, _) => fDims.width < imgDims.width && fDims.height < imgDims.height }
    // silence everything
    catch { case _: Throwable => None }
    finally g2d.dispose()
  res
}

From this code snippet, you can already see the complete Graphics2D machinery you need to get a picture instantiated. For the font size selection, we just create a dummy picture. The text dimensions and the ascent (distance of font’s baseline to top) need to be determined. Why? To center the text.

Centering the text

The code for centering the rendered text is short:

// src/main/scala/Placeholder.scala
def center(img: Dimensions, font: Dimensions, fontAscent: Int): Position =
  Position((img.width - font.width) / 2, (img.height - font.height) / 2 + fontAscent)

It is not rocket science, but still, a quick drawing can help to clarify things.

Quick Drawing

Excuse my handwriting, the equations on the right should mean:

\[ x = \frac{w - fw}{2} \qquad \text{and} \qquad y = \frac{h - fh}{2} + asc \]

with

Render the image

Right font size: check!

Center text: check!

Time to render the image. We start of with creating a grey rectangle

// src/main/scala/Placeholder.scala
val text = s"${width}x${height}"
val imgDims: Dimensions = Dimensions(width, height)
val fontWithDimsAndAscent = selectFont(imgDims, fontSizes, text)

val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g2d = img.createGraphics

g2d.setColor(Color.DARK_GRAY)
g2d.fillRect(0, 0, img.getWidth, img.getHeight)

Once that is done, we can render the text. There is still the possibility that the text would not fit in the image, because the requested dimensions are just too small (e.g. 3x3 pixels). Therefore we use foreach on the Option returned earlier in selectFont.

// src/main/scala/Placeholder.scala
fontWithDimsAndAscent.foreach { case (font, fontDims, asc) =>
  g2d.setRenderingHint(
    RenderingHints.KEY_ALPHA_INTERPOLATION,
    RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY
  )
  g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
  g2d.setRenderingHint(
    RenderingHints.KEY_COLOR_RENDERING,
    RenderingHints.VALUE_COLOR_RENDER_QUALITY
  )
  g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE)
  g2d.setRenderingHint(
    RenderingHints.KEY_FRACTIONALMETRICS,
    RenderingHints.VALUE_FRACTIONALMETRICS_ON
  )
  g2d.setRenderingHint(
    RenderingHints.KEY_INTERPOLATION,
    RenderingHints.VALUE_INTERPOLATION_BILINEAR
  )
  g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
  g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)

  g2d.setColor(Color.WHITE)
  g2d.setFont(font)
  val pos = center(imgDims, fontDims, asc)
  g2d.drawString(text, pos.x, pos.y)
}

g2d.dispose()
img

We return the image img. Now you can write the image to a file or return it as result via an endpoint.

// src/test/scala/PlaceholderSpec.scala
val img = new Placeholder(200, 100).create

try ImageIO.write(img, "png", new File("placeholder.png"))
catch {
  case ex: IOException => ex.printStackTrace()
}

All pieces together

// src/main/scala/Placeholder.scala
import Placeholder.{center, fontSizes, selectFont}

import java.awt.image.BufferedImage
import java.awt.{Color, Font, RenderingHints}


class Placeholder(width: Int, height: Int) {
  def create: BufferedImage = {
    val text = s"${width}x${height}"
    val imgDims: Dimensions = Dimensions(width, height)
    val fontWithDimsAndAscent = selectFont(imgDims, fontSizes, text)

    val img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
    val g2d = img.createGraphics

    g2d.setColor(Color.DARK_GRAY)
    g2d.fillRect(0, 0, img.getWidth, img.getHeight)

    fontWithDimsAndAscent.foreach { case (font, fontDims, asc) =>
      g2d.setRenderingHint(
        RenderingHints.KEY_ALPHA_INTERPOLATION,
        RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY
      )
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g2d.setRenderingHint(
        RenderingHints.KEY_COLOR_RENDERING,
        RenderingHints.VALUE_COLOR_RENDER_QUALITY
      )
      g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE)
      g2d.setRenderingHint(
        RenderingHints.KEY_FRACTIONALMETRICS,
        RenderingHints.VALUE_FRACTIONALMETRICS_ON
      )
      g2d.setRenderingHint(
        RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BILINEAR
      )
      g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
      g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)

      g2d.setColor(Color.WHITE)
      g2d.setFont(font)
      val pos = center(imgDims, fontDims, asc)
      g2d.drawString(text, pos.x, pos.y)
    }

    g2d.dispose()
    img
  }
}

case class Dimensions(width: Int, height: Int)
case class Position(x: Int, y: Int)

object Placeholder {
  val fontSizes = List(6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72)

  def selectFont(
      imgDims: Dimensions,
      fontSizes: List[Int],
      text: String
  ): Option[(Font, Dimensions, Int)] = {
    val img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB)
    val g2d = img.createGraphics
    val res =
      try fontSizes.reverse.view
        .map { size =>
          val font = new Font("SansSerif", Font.PLAIN, size)
          g2d.setFont(font)
          val fm = g2d.getFontMetrics

          (font, Dimensions(fm.stringWidth(text), fm.getHeight), fm.getAscent)
        }
        .find { case (_, fDims, _) => fDims.width < imgDims.width && fDims.height < imgDims.height }
      // silence everything
      catch { case _: Throwable => None }
      finally g2d.dispose()
    res
  }

  def center(img: Dimensions, font: Dimensions, fontAscent: Int): Position =
    Position((img.width - font.width) / 2, (img.height - font.height) / 2 + fontAscent)
}

You can also have a look at the example app created with akka-http.

That’s it.

Have fun!