TrueType font glyphs to SVG paths
--------------- GlyphToSVG.java ----------------
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import java.util.AbstractMap.*;
public class GlyphToSVG {
public static void main(String argv[]) { new GlyphToSVG(); }
GlyphToSVG() {
font(new File("Noto.ttf"), -1);
}
void font(File fl, int ind) {
//System.out.println(fl.getAbsolutePath());
try (RandomAccessFile randomAccessFile = new RandomAccessFile(fl, "r")) {
font(randomAccessFile, ind);
} catch (Exception ex) { ex.printStackTrace(); }
}
RandomAccessFile raf;
long offTHead = -1, offTMaxp = -1, offTLoca = -1, offTGlyf = -1;
void font(RandomAccessFile raf, int ind) throws IOException {
this.raf = raf;
long sfntVersion = uint32();
if (sfntVersion != 0x74727565 && sfntVersion != 0x00010000) throw new IOException("only True Type font format supported");
int numTables = uint16();
uint16(); //searchRange
uint16(); //entrySelector
uint16(); //rangeShift
for (int i = 0; i < numTables; i++) {
String tableTag = new String(readBytes(4), StandardCharsets.ISO_8859_1);
uint32(); //checksum
long offset = offset32();
uint32(); //length
switch (tableTag) {
case "head" : offTHead = offset; break;
case "maxp" : offTMaxp = offset; break;
case "loca" : offTLoca = offset; break;
case "glyf" : offTGlyf = offset; break;
}
}
if (offTHead < 0 || offTMaxp < 0 || offTLoca < 0 || offTGlyf < 0) {
throw new IOException("required tables are missing");
}
head();
maxp();
loca();
if (ind >= 0 && ind < numGlyphs) glyph(ind);
else if (ind == -1) glyphs();
else if (ind == -2) for (int i = 0; i < numGlyphs; i++) glyph(i);
else throw new IOException("bad ind " + ind);
}
void glyph(int ind) throws IOException {
List<Map.Entry<Character, int[]>> lp = glyphPath(ind);
double k = 1; // 1000d / unitsPerEm;
transform(lp, k, 0d, 0d, -k, 0, 0);
try (PrintWriter out = new PrintWriter("glyph" + ind + ".svg")) {
int x = (int)(xMin * k), y = (int)(-yMax * k), w = (int)((xMax - xMin) * k), h = (int)((yMax - yMin) * k);
out.println("<svg width=\"100%\" height=\"100%\" viewBox=\"" + x + " " + y + " " + w + " " + h + "\" xmlns=\"http://www.w3.org/2000/svg\">");
out.println(" <rect x=\"" + x + "\" y=\"" + y + "\" width=\"" + w + "\" height=\"" + h + "\" stroke=\"black\" fill=\"transparent\"/>");
out.println(" <path d=\"" + PathToString(lp) + "\"/>");
out.println("</svg>");
}
}
void glyphs() throws IOException {
try (PrintWriter out = new PrintWriter("glyphs.html")) {
out.println("<html><head></head><body>");
for (int i = 0; i < numGlyphs; i++) {
List<Map.Entry<Character, int[]>> lp = glyphPath(i);
int offx= 5 + (i % 10) * 160;
int offy= 5 + (i / 10) * 160;
int x = xMin, y = -yMax, w = xMax - xMin, h = yMax - yMin;
transform(lp, 1, 0d, 0d, -1, 0, 0);
out.println("<div style=\"position: absolute; top: " + offy + "px; left: " + offx + "px;\" title=\"" + i + "\">");
out.println(" <svg width=\"150\" height=\"150\" viewBox=\"" + x + " " + y + " " + w + " " + h + "\" xmlns=\"http://www.w3.org/2000/svg\">");
out.println(" <rect x=\"" + x + "\" y=\"" + y + "\" width=\"" + w + "\" height=\"" + h + "\" stroke=\"black\" fill=\"transparent\"/>");
out.println(" <path d=\"" + PathToString(lp) + "\"/>");
out.println(" </svg>");
out.println("</div>");
}
out.println("</body></html>");
}
}
String PathToString(List<Map.Entry<Character, int[]>> lp) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Character, int[]> en : lp) {
sb.append(en.getKey().charValue());
int[] arr = en.getValue();
for (int i = 0; i < arr.length; i++) {
if (i > 0 && arr[i] >= 0) sb.append(' ');
sb.append(arr[i]);
}
}
return sb.toString();
}
List<Map.Entry<Character, int[]>> glyphPath(int ind) {
List<Map.Entry<Character, int[]>> lp = new ArrayList<>();
if (glyphOffsets[ind] != glyphOffsets[ind + 1]) try {
raf.seek(offTGlyf + glyphOffsets[ind]);
short numberOfContours = int16();
int16(); //xMin
int16(); //yMin
int16(); //xMax
int16(); //yMax
if (numberOfContours > 0) { //simple glyphs
int[] endPtsOfContours = new int[numberOfContours];
int pointCount = 0;
for (int i = 0; i < numberOfContours; i++) {
endPtsOfContours[i] = uint16();
}
pointCount = endPtsOfContours[numberOfContours - 1] + 1;
int instructionLength = uint16();
readBytes(instructionLength); //instructions
int[] flags = new int[pointCount];
boolean[] onCurve = new boolean[pointCount];
int[] x = new int[pointCount];
int[] y = new int[pointCount];
int flag, repeatValue;
for (int i = 0; i < pointCount; i++) {
flag = uint8();
flags[i] = flag;
boolean repeat = ((flag & 0x0008) > 0);
if (repeat) {
repeatValue = uint8();
for (int j = 0; j < repeatValue; j++) {
i++;
flags[i] = flag;
}
}
}
for (int px = 0, i = 0; i < pointCount; i++) {
onCurve[i] = ((flags[i] & 0x0001) > 0);
x[i] = px += readCoord(flags[i], 0x0002, 0x0010);
}
for (int py = 0, i = 0; i < pointCount; i++) {
y[i] = py += readCoord(flags[i], 0x0004, 0x0020);
}
int x2 = 0, y2 = 0;
int startIndex, endIndex, prev, curr, next;
for (int p = 0, i = 0; i < numberOfContours; i++) {
startIndex = p;
endIndex = (i == numberOfContours - 1 ? pointCount - 1 : endPtsOfContours[i]);
curr = endIndex;
next = startIndex;
if (onCurve[curr]) { x2 = x[curr]; y2 = y[curr]; }
else if (onCurve[next]) { x2 = x[next]; y2 = y[next]; }
else { x2 = (x[curr] + x[next]) / 2; y2 = (y[curr] + y[next]) / 2; }
lp.add(new SimpleEntry<>(Character.valueOf('M'), new int[] { x2, y2 }));
while (p <= endPtsOfContours[i]) {
prev = curr;
curr = next;
next = (p == endIndex ? startIndex : (p + 1));
if (onCurve[curr]) {
if (x2 != x[curr] || y2 != y[curr]) {
x2 = x[curr]; y2 = y[curr];
lp.add(new SimpleEntry<>(Character.valueOf('L'), new int[] { x2, y2 }));
}
} else {
x2 = x[next];
y2 = y[next];
if (!onCurve[next]) {
x2 = (x[curr] + x2) / 2;
y2 = (y[curr] + y2) / 2;
}
lp.add(new SimpleEntry<>(Character.valueOf('Q'), new int[] { x[curr], y[curr], x2, y2 }));
}
p++;
}
lp.add(new SimpleEntry<>(Character.valueOf('Z'), new int[] { }));
}
} else { //composite glyphs
int flags, glyphIndex, dx, dy;
do {
flags = uint16();
glyphIndex = uint16();
long ptbl = raf.getFilePointer();
List<Map.Entry<Character, int[]>> cgp = glyphPath(glyphIndex);
raf.seek(ptbl);
if ((flags & 0x0001) > 0) {
dx = int16();
dy = int16();
} else {
dx = int8();
dy = int8();
}
if ((flags & 0x0008) > 0) {
double scale = f2d14();
transform(cgp, scale, 0d, 0d, scale, dx, dy);
} else if ((flags & 0x0040) > 0) {
double xscale = f2d14();
double yscale = f2d14();
transform(cgp, xscale, 0d, 0d, yscale, dx, dy);
} else if ((flags & 0x0080) > 0) {
double xscale = f2d14();
double scale01 = f2d14();
double scale10 = f2d14();
double yscale = f2d14();
transform(cgp, xscale, scale01, scale10, yscale, dx, dy);
} else if (dx != 0 || dy != 0) {
transform(cgp, 1d, 0d, 0d, 1d, dx, dy);
}
lp.addAll(cgp);
} while ((flags & 0x0020) > 0);
}
} catch (Exception ex) { System.out.println("faulty glyph " + ind); }
return lp;
}
int readCoord(int flag, int mask1, int mask2) throws IOException {
int v = 0;
if ((flag & mask1) == 0) {
if ((flag & mask2) == 0) v = int16();
} else {
v = uint8();
if ((flag & mask2) == 0) v = -v;
}
return v;
}
void transform(List<Map.Entry<Character, int[]>> cgp, double xscale, double scale01, double scale10, double yscale, int dx, int dy) {
for (Map.Entry<Character, int[]> en : cgp) {
int[] arr = en.getValue();
for (int i = 0; i < arr.length; i += 2) {
int x = arr[i], y = arr[i + 1];
arr[i] = (int)(x * xscale + y * scale01) + dx;
arr[i + 1] = (int)(y * yscale + x * scale10) + dy;
}
}
}
short xMin, yMin, xMax, yMax;
short indexToLocFormat;
void head() throws IOException {
raf.seek(offTHead + 36);
xMin = int16();
yMin = int16();
xMax = int16();
yMax = int16();
uint16(); //macStyle
uint16(); //lowestRecPPEM
int16(); //fontDirectionHint
indexToLocFormat = int16();
}
int numGlyphs;
void maxp() throws IOException {
raf.seek(offTMaxp + 4);
numGlyphs = uint16();
}
long[] glyphOffsets;
void loca() throws IOException {
raf.seek(offTLoca);
glyphOffsets = new long[numGlyphs + 1];
if (indexToLocFormat == 0) {
for (int i = 0; i < glyphOffsets.length; i++) glyphOffsets[i] = offset16() * 2;
} else {
for (int i = 0; i < glyphOffsets.length; i++) glyphOffsets[i] = offset32();
}
}
int uint8() throws IOException {
int b = raf.read();
if (b < 0) throw new EOFException();
return b;
}
byte int8() throws IOException { return (byte) uint8(); }
int uint16() throws IOException {
int b1 = raf.read(), b2 = raf.read();
if ((b1 | b2) < 0) throw new EOFException();
return (b1 << 8) + b2;
}
short int16() throws IOException { return (short) uint16(); }
long uint32() throws IOException {
long b1 = raf.read(), b2 = raf.read(), b3 = raf.read(), b4 = raf.read();
if ((b1 | b2 | b3 | b4) < 0) throw new EOFException();
return (b1 << 24) + (b2 << 16) + (b3 << 8) + b4;
}
// int int32() throws IOException { return (int) uint32(); }
double f2d14() throws IOException {
int b1 = raf.read(), b2 = raf.read();
if ((b1 | b2) < 0) throw new EOFException();
int m = b1 >> 6;
if (m >= 2) m -= 4;
return (double)m + (double)(((b1 & 0x3f) << 8) + b2) / 16384d;
}
byte[] readBytes(int len) throws IOException {
byte[] buf = new byte[len];
if (raf.read(buf) != buf.length) throw new EOFException();
return buf;
}
long offset32() throws IOException { return uint32(); }
int offset16() throws IOException { return uint16(); }
}