GlyphOutlines.java

/* GlyphOutlines.java
 * =========================================================================
 * Reader of the glyphs.bin files (outlines of the JLaTeXMath fonts).
 *
 * This file is NOT part of upstream JLaTeXMath, it was added for PLAN.md
 * (phase 2, step 2b): a glyph is a path, not a call to the font engine of AWT.
 * It is released under the same terms as the library: GNU GPL version 2 or
 * later, with the GNU linking exception (see the header of the other files of
 * this package).
 * =========================================================================
 */

package org.scilab.forge.jlatexmath;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * The outlines of the glyphs of the TeX fonts, read from a {@code glyphs.bin}
 * file made by {@code tools/jlatexmath-glyphs/GenerateGlyphOutlines.java} (the
 * format is described there).
 * <p>
 * No dependency on {@code java.awt}, {@code javax.*} or reflection: the same
 * code runs on the JVM and in the TeaVM builds, and gives the same numbers
 * everywhere. A glyph is given to a {@link Sink} as a sequence of absolute
 * points, in font units, y axis up.
 */
public final class GlyphOutlines {

	private static final int MOVE = 0;
	private static final int LINE = 1;
	private static final int QUAD = 2;
	private static final int CLOSE = 3;
	private static final int END = 4;

	/** Receives the outline of a glyph. */
	public interface Sink {

		void moveTo(int x, int y);

		void lineTo(int x, int y);

		/** Quadratic curve from the current point. */
		void quadTo(int controlX, int controlY, int x, int y);

		void close();
	}

	/** The glyphs of one font (one TTF file). */
	public static final class GlyphFont {

		private final byte[] data;
		private final String name;
		private final int unitsPerEm;
		/** The codes, in increasing order. */
		private final int[] codes;
		/** For each code, the offset in data of its first command. */
		private final int[] starts;

		private GlyphFont(byte[] data, String name, int unitsPerEm, int[] codes, int[] starts) {
			this.data = data;
			this.name = name;
			this.unitsPerEm = unitsPerEm;
			this.codes = codes;
			this.starts = starts;
		}

		/** The name of the TTF file, without ".ttf". */
		public String getName() {
			return name;
		}

		/** The size of the em square: one unit of the outlines is 1/unitsPerEm em. */
		public int getUnitsPerEm() {
			return unitsPerEm;
		}

		public int getNumberOfGlyphs() {
			return codes.length;
		}

		public boolean hasGlyph(int code) {
			return indexOf(code) >= 0;
		}

		/**
		 * Gives the outline of a glyph to the sink.
		 *
		 * @return false if the font has no glyph for this code (nothing is given to
		 *         the sink); a blank glyph (a space) returns true and draws nothing.
		 */
		public boolean visit(int code, Sink sink) {
			final int index = indexOf(code);
			if (index < 0)
				return false;
			final int[] pos = { starts[index] };
			int x = 0;
			int y = 0;
			while (true) {
				final int command = data[pos[0]++] & 0xFF;
				switch (command) {
				case MOVE:
					x += varint(data, pos);
					y += varint(data, pos);
					sink.moveTo(x, y);
					break;
				case LINE:
					x += varint(data, pos);
					y += varint(data, pos);
					sink.lineTo(x, y);
					break;
				case QUAD:
					final int cx = x + varint(data, pos);
					final int cy = y + varint(data, pos);
					x = cx + varint(data, pos);
					y = cy + varint(data, pos);
					sink.quadTo(cx, cy, x, y);
					break;
				case CLOSE:
					sink.close();
					break;
				case END:
					return true;
				default:
					throw new IllegalStateException("Bad command " + command + " in glyphs of " + name);
				}
			}
		}

		private int indexOf(int code) {
			int low = 0;
			int high = codes.length - 1;
			while (low <= high) {
				final int mid = (low + high) >>> 1;
				if (codes[mid] < code)
					low = mid + 1;
				else if (codes[mid] > code)
					high = mid - 1;
				else
					return mid;
			}
			return -1;
		}
	}

	private final Map<String, GlyphFont> fonts = new HashMap<String, GlyphFont>();

	private GlyphOutlines() {
	}

	/** The names of the fonts of this file (TTF file names without ".ttf"). */
	public Set<String> getFontNames() {
		return Collections.unmodifiableSet(fonts.keySet());
	}

	/** The font of a TTF file name, with or without ".ttf", or null. */
	public GlyphFont getFont(String ttfName) {
		if (ttfName.endsWith(".ttf"))
			ttfName = ttfName.substring(0, ttfName.length() - 4);
		return fonts.get(ttfName);
	}

	// ------------------------------------------------------------------ loading

	private static final Map<Class<?>, GlyphOutlines> CACHE = new HashMap<Class<?>, GlyphOutlines>();

	/**
	 * The outlines that go with the fonts of a registration object: the file
	 * {@code fonts/glyphs.bin} of the package of its class. {@code base} is null
	 * for the base fonts, the registration object for an alphabet (Cyrillic,
	 * Greek), as in {@link FontInfo}.
	 * <p>
	 * Read on the first use only, nothing is parsed by a diagram without math.
	 */
	public static synchronized GlyphOutlines forBase(Object base) {
		final Class<?> clazz = base == null ? GlyphOutlines.class : base.getClass();
		GlyphOutlines result = CACHE.get(clazz);
		if (result == null) {
			// TODO phase 3: resource abstraction (class path, EmbeddedFiles, bundle).
			final InputStream in = TeXResources.open(clazz, "fonts/glyphs.bin");
			if (in == null)
				throw new IllegalStateException("No fonts/glyphs.bin next to " + clazz.getName());
			try {
				result = parse(readAll(in));
			} catch (IOException e) {
				throw new IllegalStateException("Cannot read fonts/glyphs.bin of " + clazz.getName(), e);
			} finally {
				try {
					in.close();
				} catch (IOException e) {
					// Nothing to do
				}
			}
			CACHE.put(clazz, result);
		}
		return result;
	}

	private static byte[] readAll(InputStream in) throws IOException {
		byte[] buffer = new byte[1 << 16];
		int size = 0;
		while (true) {
			if (size == buffer.length) {
				final byte[] bigger = new byte[buffer.length * 2];
				System.arraycopy(buffer, 0, bigger, 0, size);
				buffer = bigger;
			}
			final int n = in.read(buffer, size, buffer.length - size);
			if (n < 0)
				break;
			size += n;
		}
		final byte[] result = new byte[size];
		System.arraycopy(buffer, 0, result, 0, size);
		return result;
	}

	/** Parses the content of a glyphs.bin file: finds the glyphs, decodes none. */
	public static GlyphOutlines parse(byte[] data) {
		if (data.length < 7 || data[0] != 'J' || data[1] != 'L' || data[2] != 'M' || data[3] != 'G')
			throw new IllegalArgumentException("Not a glyphs.bin file");
		if (data[4] != 1)
			throw new IllegalArgumentException("Unknown glyphs.bin version " + data[4]);
		final GlyphOutlines result = new GlyphOutlines();
		final int[] pos = { 5 };
		final int numberOfFonts = u16(data, pos);
		for (int f = 0; f < numberOfFonts; f++) {
			final int nameLength = u16(data, pos);
			// DataOutputStream.writeUTF: the names are plain ASCII
			final char[] chars = new char[nameLength];
			for (int i = 0; i < nameLength; i++)
				chars[i] = (char) (data[pos[0]++] & 0xFF);
			final String name = new String(chars);
			final int unitsPerEm = u16(data, pos);
			final int numberOfGlyphs = u16(data, pos);
			final int[] codes = new int[numberOfGlyphs];
			final int[] starts = new int[numberOfGlyphs];
			for (int g = 0; g < numberOfGlyphs; g++) {
				codes[g] = u16(data, pos);
				starts[g] = pos[0];
				skipOutline(data, pos);
				if (g > 0 && codes[g] <= codes[g - 1])
					throw new IllegalArgumentException("Codes not sorted in " + name);
			}
			result.fonts.put(name, new GlyphFont(data, name, unitsPerEm, codes, starts));
		}
		return result;
	}

	private static void skipOutline(byte[] data, int[] pos) {
		while (true) {
			final int command = data[pos[0]++] & 0xFF;
			switch (command) {
			case MOVE:
			case LINE:
				varint(data, pos);
				varint(data, pos);
				break;
			case QUAD:
				varint(data, pos);
				varint(data, pos);
				varint(data, pos);
				varint(data, pos);
				break;
			case CLOSE:
				break;
			case END:
				return;
			default:
				throw new IllegalArgumentException("Bad command " + command);
			}
		}
	}

	private static int u16(byte[] data, int[] pos) {
		final int result = ((data[pos[0]] & 0xFF) << 8) | (data[pos[0] + 1] & 0xFF);
		pos[0] += 2;
		return result;
	}

	/** Zigzag varint. */
	private static int varint(byte[] data, int[] pos) {
		int result = 0;
		int shift = 0;
		while (true) {
			final int b = data[pos[0]++] & 0xFF;
			result |= (b & 0x7F) << shift;
			if ((b & 0x80) == 0)
				break;
			shift += 7;
		}
		return (result >>> 1) ^ -(result & 1);
	}
}