TrueType font glyphs to SVG paths

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(); }
}

Download ZIP

Back