Bitmap font tileset with Ruby and Imagemagick

My latest undertaking is writing mobile apps with Java ME. A problem I bumped into was creating tiles for bitmap fonts to use for displaying text in an application, in lieu of the Graphics.drawString() method, which may not always produce the most suitable output. Here I show you how you can use a Ruby script combined with ImageMagick to create a usable bitmap font tileset for your app.

(Skip the boring story and get to the meat!)

One of the things you soon discover when writing mobile apps with JME is that if you want to write professional-looking games, for example, you can’t rely on the Graphics class’ drawString() method to deliver good-looking text. Instead, you need to use bitmap fonts to deliver a consistent look across platforms. To this end, there are some useful articles around, including this one from Sun Developer Network and another one from Devlin’s Lab (a blog on Blogspot).

To do this, however, you need to have a bitmap font tileset, for which you need, firstly, a font, and secondly, a tool to create an image from that font with all the necessary ASCII characters. The first thing that obviously springs to mind is ImageMagick.

I have lately gained more and more respect for ImageMagick, with its overwhelming plethora of powerful options, and it has proved to be quite useful on a few occasions. One of the things you can do with it is to create an image of text, which would be useful to create a bitmap font tileset. The problem is that even for fixed width fonts, it seems to create the characters in different widths (brackets are narrower, for example). This is a problem, because in your tileset, you require all characters to have the same width.

I looked around for an answer to this, and unless I missed something glaringly obvious, it seems there is no straightforward to do this with the ImageMagick tools. My findings seem to be corroborated by this forum post.

So I embarked on writing a script to generate such a tileset. What it does is this: For each of the printable ASCII characters that you would use (there are 95 of them – from decimal 32 to 126), it creates an image of the ASCII character. It then uses the montage command to create a tileset using all images when it has determined the height and width of the biggest of them.

Fair enough, the script is longer than the perl one-liner in the forum post, but it does a bit more. For starters, I create a one-character file to generate each character image, because issuing certain characters on the command line confuses the convert command. Also, it allows the user to specify a font, point size, colour and whether or not to use anti-aliasing (which can look better, but makes your tileset file significantly bigger – we are talking kilobytes here, but remember, it is intended for mobile apps).

(For the available fonts, the script looks for .ttf files in the current directory, but you can of course specify any other available font on the system).

So anyway, a successful session of bmfontgen.rb, as the script is called, could look as follows:

Specify a font to use or choose one from the list below:
(Run 'convert -list font' to get a list of valid fonts)
[0] VeraMono.ttf
[1] LiberationMono-Regular.ttf
[2] Anonymous.ttf
Specify font to use [0]: Courier
Specify point size to use [use default]: 9
For a list of colours, run 'convert -list Color'
Specify colour to use [black]: 
Use ant-aliasing? (yes/no) [yes]: no
Working...
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~Creating tilemap...
Done. Created file /tmp/bmfontgen/Courier_8x9.png

The filename produced tells you the size (WxH) of the tiles, so you can use it in your project, here being an example of an antialiased font on an emulated phone:

Text from bitmap font shown on phone

Text from bitmap font shown on phone

That’s it! Below, I give you the script in all its glory, and a zip download of the script plus three free (and free to use/distribute) fixed-width fonts – Anonymous, Bitstream Vera and Liberation – to go with it. You can of course use proportional fonts as well if you wish, but I thought that monospaced fonts work better for this purpose.

First, here is the script, which you can just invoke with rb bmfontgen.rb (or whatever you choose to call it). You need to of course have Ruby and ImageMagick installed, with its binaries in your search path.

#!/usr/bin/ruby
# Script to generate bitmap font tilesets for use
# e.g. in mobile applications, using printable ASCII characters
# 32-126 (95 in all)
# Requires you to have ImageMagick installed and have
# the binaries in your search path
# Author: Martin Ceronio - http://ceronio.net July 2009

# Caveat: Ruby unfortunately does not cater for executing a command
# using a string with backquotes (``). Also, popen() is not available
# on Windows (at least for 1.8.6). The downside is that the directory
# must be /tmp/bmfontgen/ and the temp file /tmp/bmfontgen/_.png
# which makes the tmpdir variable a bit pointless
# (Having the standard output is necessary to determine the filesize)

require "fileutils"

tmpdir = "/tmp/bmfontgen/"
charfl = tmpdir + "lett" #File containing a single ASCII character

# Variables to hold the maximum width and height of the generated
# character graphics
maxw = 0
maxh = 0

# Create temporary directory for our work and clean it out
FileUtils.mkdir_p tmpdir
FileUtils.rm Dir.glob("#{tmpdir}*")

trap("INT"){ exit } #Exit cleanly if Ctrl-C is pressed

# Get input from user

# Ask user to select a font for rendering
fonts = {}
fontno = 0;
puts "Specify a font to use or choose one from the list below:"
puts "(Run 'convert -list font' to get a list of valid fonts)"
Dir.glob("*.ttf").each { |filename|
  fonts[fontno.to_s] = filename
  puts "[#{fontno}] #{filename}"
  fontno +=1
}
print "Specify font to use [0]: "
gets
$_.chomp!
$_ = "0" if $_ == ""
fonts[$_] ? fontname = fonts[$_] : fontname = $_

# Get point size from user
print "Specify point size to use [use default]: "
gets
$_ == "\n" ? pointsize = nil : pointsize = $_.chomp!

# Get colour from user
# For U.S.A., change 'colour' to 'color' :-)
puts "For a list of colours, run 'convert -list Color'"
print "Specify colour to use [black]: "
gets
$_ == "\n" ? color = nil : color = $_.chomp!

# Ask user to specify whether to use anti-aliasing
print "Use ant-aliasing? (yes/no) [yes]: "
gets
antialias = nil
antialias = true if $_ == "\n" or $_ == "yes"

# Status message
puts "Working..."

# Loop through each of the printable ASCII characters for
# which we want to create an image
(32...127).each { |n|

# Create a file with a single character that ImageMagick will use
  open(charfl, 'w') {|f| f << n.chr; f.close }

# Filename of the output file of the image
# It will either be 2 or 3 chars long before the .png extension,
# if it is 2, give it a leading 0 so that the sort order is correct
  cs = n.to_s
  cs = "0" + cs if cs.length == 2

  # Set up command and options to create character graphic
  command = "convert"
  command << " -font #{fontname}"              #Font to use
  command << " -fill #{color}" if color        #Color font
  command << " -pointsize #{pointsize}" if pointsize    #Point size
  command << " +antialias" unless antialias    #Use antialiasing?
  command << " -background none -channel RGBA" #Make transparent background
  command << " label:@#{charfl}"               #File containing the character
  command << " +antialias" unless antialias    #Use antialiasing?
  command << " /tmp/bmfontgen/_.png"  #Temporary Output file


  # Execute command to make the character graphic
  system command

  # Now we will query the character graphic to determine its size:

  ## Set up command and options to query character graphic information
  #command = "identify"
  #command << " #{tmpdir}#{cs}.png" #Name of the file we just created
  ## Execute 'identify' command to get graphic file info
  ## OOPS! BACKQUOTE EXECUTION DOES NOT ACCEPT A STRING! (SEE CAVEAT ABOVE)

  info = `identify /tmp/bmfontgen/_.png`

  # Strip the dimensions information out of the info
  info =~ /PNG ([0-9]*x[0-9]*)/
  w, h = $1.split('x')
  maxw = w.to_i if w.to_i > maxw
  maxh = h.to_i if h.to_i > maxh

  # Rename the temp file to (ascii char).png
  FileUtils.mv "/tmp/bmfontgen/_.png", "#{tmpdir}#{cs}.png"

  # Status update
  print n.chr

}

# Status update
puts "Creating tilemap..."

# Use the ImageMagick 'montage' command to create the tileset
outfile = "#{fontname}_#{maxw}x#{maxh}#{"_antialiased" if antialias}.png"
command = "montage"
command << " +frame +shadow -label \"\""           #Ensure no funny business
command << " #{tmpdir}*.png"                       #The files to process
command << " -background none -channel RGBA"       #Retain transparent background
command << " -tile 95x1 -geometry #{maxw}x#{maxh}" #Output tile and image dimensions
command << " #{tmpdir}#{outfile}"                  #Name of the finished product
system command

# Clean up the many little files we created
FileUtils.rm(charfl)
(32...127).each { |n|
  cs = n.to_s
  cs = "0" + cs if cs.length == 2
  FileUtils.rm("#{tmpdir}#{cs}.png")
}

# Status update
puts "Done. Created file #{tmpdir}#{outfile}"

And here is the zip file.

Tags: , ,