
net.freeutils.scrollphat.FontConverter Maven / Gradle / Ivy
/*
* Copyright © 2016 Amichai Rothman
*
* This file is part of JScrollPhat - the Java Scroll pHAT package.
*
* JScrollPhat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JScrollPhat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JScrollPhat. If not, see .
*
* For additional info see http://www.freeutils.net/source/jscrollphat/
*/
package net.freeutils.scrollphat;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Converts a standard TrueType (TTF) font, rendered at a given size,
* to the ledfont format used at runtime by the LEDFont class.
*
* In order to conserve resources on resource-limited devices (such as
* the Raspberry Pi), this utility can be used manually during development
* to generate font files in the lightweight format used by LEDFont,
* which in turn can be used during runtime to render text using the font.
*
* Note that most standard desktop fonts look terrible, and often illegible,
* when rendered at tiny sizes for which they were not designed.
* However, if you look around you can find a dozen or two fonts that were
* designed specifically for tiny sizes and will look good even in a target
* font height of e.g. 5 pixels. Some of these are categorized as pixel fonts.
*
* You may need to try various point sizes, one at a time, until you find
* the one that looks best for your target font height in pixels. For example,
* several fonts look best when rendered at a size of 8 points and cropped
* to a target height of 5 pixels (due ascent/descent spaces, etc.)
*
* Please check the licensing terms of the fonts you use. Most of them are
* free for personal use, and many are also free for commercial use, but
* you'll need to verify compliance on a case-by-case basis.
* If you create your own fonts, please consider sharing them freely as well!
*
* The {@code -d} or {@code -t} command line arguments can be used to print
* the converted font glyphs to the console to quickly assess how it looks.
*
* The font pixel data is currently stored using one byte per column,
* i.e. there is a font height limit of 8 pixels.
*/
public class FontConverter {
/**
* Loads a TrueType (TTF) font from file or resource as an AWT Font.
*
* @param filename the file (or resource) name
* @return the font
* @throws IOException if an error occurs
*/
public static Font loadTTF(String filename) throws IOException {
InputStream in = null;
try {
in = Utils.getInputStream(filename);
return Font.createFont(Font.TRUETYPE_FONT, in);
} catch (FontFormatException ffe) {
throw new IOException(ffe.toString());
} finally {
if (in != null)
in.close();
}
}
/**
* Converts a single glyph's pixels into ledfont column data.
*
* @param out the array to write to (must be at least as large as width)
* @param image an image on which the character's glyph is drawn
* @param baseline the glyph's baseline (in pixels from the top)
* @param width the glyph's width
* @param height the glyph's height
* @throws IOException if an error occurs
*/
static void convertPixels(byte[] out, BufferedImage image, int baseline, int width, int height) throws IOException {
int imageHeight = image.getHeight();
for (int col = 0; col < width; col++) {
byte b = 0;
for (int row = 0; row < height; row++) {
int y = baseline - height + row;
if (y >= 0 && y < imageHeight) {
int rgb = image.getRGB(col, y) & 0x00ffffff;
if (rgb != 0)
b |= (1 << row);
}
}
out[col] = b;
}
}
/**
* Converts the given font to ledfont format written to a LEDFont.
*
* @param font the AWT font to convert
* @param height the target height in pixels
* @param offset an offset by which all glyphs should be raised or lowered
* relative to the font baseline (this should usually be zero)
* @return the converted LEDFont
* @throws IOException if an error occurs
*/
public static LEDFont convert(Font font, int height, int offset) throws IOException {
// prepare temp buffered image
int imageWidth = height * 4;
int imageHeight = height * 4;
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
g.setFont(font);
FontMetrics metrics = g.getFontMetrics();
int baseline = metrics.getAscent();
if (baseline == 0) // a bug in the ttf file?
baseline = height;
// create font
LEDFont ledfont = new LEDFont(height);
// process all chars
char[] chars = new char[2];
byte[] buf = new byte[256];
for (int c = 0; c <= Character.MAX_CODE_POINT; c++) {
if (font.canDisplay(c)) {
// draw glyph
g.setColor(Color.BLACK);
g.fillRect(0, 0, imageWidth, imageHeight);
g.setColor(Color.WHITE);
int charCount = Character.toChars(c, chars, 0);
g.drawChars(chars, 0, charCount, 0, baseline);
Rectangle2D bounds = metrics.getStringBounds(chars, 0, charCount, g);
int width = (int)bounds.getWidth();
// write entry
if (width > 0) {
convertPixels(buf, image, baseline - offset, width, height);
ledfont.addChar(c, buf, 0, width);
}
}
}
g.dispose();
return ledfont;
}
/**
* Converts the given font to ledfont format written to a file.
*
* @param font the AWT font to convert
* @param height the target height in pixels
* @param offset an offset by which all glyphs should be raised or lowered
* relative to the font baseline (this can usually be left at zero)
* @param out the file to which the ledfont data is written
* @throws IOException if an error occurs
*/
public static void convert(Font font, int height, int offset, File out) throws IOException {
convert(font, height, offset).save(out);
}
/**
* Parses a python script defining a font data structure.
*
* @param in the input stream containing the python script
* @param height the font height
* @return the parsed LEDFont
* @throws IOException if an error occurs
*/
public static LEDFont parsePython(InputStream in, int height) throws IOException {
String script = new String(Utils.readBytes(in), "UTF-8");
Matcher matcher = Pattern.compile("\\{(\\s*(\\d+)\\s*:\\s*\\[([\\s\\d,]*)\\][\\s,]*)*\\}").matcher(script);
if (!matcher.find())
throw new IOException("invalid python font definition");
script = matcher.group().replaceAll("\\s", "");
LEDFont ledfont = new LEDFont(height);
byte[] buf = new byte[256];
ledfont.addChar(' ', buf, 0, 3); // space char is hard-coded
matcher = Pattern.compile("\\s*(\\d+)\\s*:\\s*\\[([\\s\\d,]*)\\][\\s,]*").matcher(script);
while (matcher.find()) {
int codePoint = Integer.parseInt(matcher.group(1));
String data = matcher.group(2).replace("\\s", "");
if (data.length() > 0) {
String[] cols = data.split(",");
int len = cols.length;
for (int i = 0; i < len; i++)
buf[i] = Byte.parseByte(cols[i]);
buf[len++] = 0; // space between chars is hard-coded
ledfont.addChar(codePoint, buf, 0, len);
}
}
return ledfont;
}
/**
* Converts a python script defining a font data structure to a LEDFont.
*
* @param in the input file containing the python script
* @param height the font height
* @param out the file to which the ledfont data is written
* @throws IOException if an error occurs
*/
public static void convertFromPython(File in, int height, File out) throws IOException {
parsePython(new FileInputStream(in), height).save(out);
}
/**
* The main command-line utility entry point.
*
* @param args the arguments
* @throws IOException if an error occurs
*/
public static void main(String[] args) throws IOException {
// parse args
String in = null;
File out = null;
float points = 8;
int height = 5;
int offset = 0;
boolean dump = false;
String text = null;
int i = 0;
while (i < args.length) {
String arg = args[i++];
if (i == args.length || arg.length() < 2 || arg.charAt(0) != '-')
throw new IllegalArgumentException("invalid argument: " + arg);
String val = args[i++];
switch (arg.charAt(1)) {
case 'i': in = val; break;
case 'o': out = new File(val); break;
case 'p': points = Float.parseFloat(val); break;
case 'h': height = Integer.parseInt(val); break;
case 'f': offset = Integer.parseInt(val); break;
case 't': text = val; break;
case 'd': dump = val.equals("true"); break;
default: throw new IllegalArgumentException("invalid argument: " + arg);
}
}
if (in == null) {
System.out.println("Usage: FontConverter -i input [-o output] [-p points] [-h height] [-f offset] [-t text] [-d true]");
System.exit(-1);
}
if (out == null)
out = new File(in).getAbsoluteFile().getParentFile();
if (out.isDirectory())
out = new File(out, new File(in).getName().replaceAll("[.][^.]+$", "") + ".ledfont");
// process font conversion
if (in.toLowerCase().endsWith(".py")) {
convertFromPython(new File(in), height, out);
} else {
Font font = loadTTF(in);
font = font.deriveFont(font.getStyle(), points);
convert(font, height, offset, out);
}
if (text != null || dump) {
LEDFont ledfont = new LEDFont(out.getAbsolutePath());
if (dump)
text = ledfont.getSupportedCharsAsString();
System.out.println(ledfont.toAsciiArt(text, 80));
}
}
}