All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogic.gdx.tools.hiero.Kerning Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.tools.hiero;

import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntIntMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/** Reads a TTF font file and provides access to kerning information.
 * 
 * Thanks to the Apache FOP project for their inspiring work!
 * 
 * @author Nathan Sweet */
public class Kerning {
	private TTFInputStream input;
	private float scale;
	private int headOffset = -1;
	private int kernOffset = -1;
	private int gposOffset = -1;
	private IntIntMap kernings = new IntIntMap();

	/** @param inputStream The data for the TTF font.
	 * @param fontSize The font size to use to determine kerning pixel offsets.
	 * @throws IOException If the font could not be read. */
	public void load (InputStream inputStream, int fontSize) throws IOException {
		if (inputStream == null) throw new IllegalArgumentException("inputStream cannot be null.");
		input = new TTFInputStream(inputStream);
		inputStream.close();

		readTableDirectory();
		if (headOffset == -1) throw new IOException("HEAD table not found.");
		readHEAD(fontSize);

		// By reading the 'kern' table last, it takes precedence over the 'GPOS' table. We are more likely to interpret
		// the GPOS table incorrectly because we ignore most of it, since BMFont doesn't support its features.
		if (gposOffset != -1) {
			input.seek(gposOffset);
			readGPOS();
		}
		if (kernOffset != -1) {
			input.seek(kernOffset);
			readKERN();
		}
		input.close();
		input = null;
	}

	/** @return A map from pairs of glyph codes to their kerning in pixels. Each map key encodes two glyph codes: the high 16 bits
	 *         form the first glyph code, and the low 16 bits form the second. */
	public IntIntMap getKernings () {
		return kernings;
	}

	private void storeKerningOffset (int firstGlyphCode, int secondGlyphCode, int offset) {
		// Scale the offset values using the font size.
		int value = Math.round(offset * scale);
		if (value == 0) {
			return;
		}
		int key = (firstGlyphCode << 16) | secondGlyphCode;
		kernings.put(key, value);
	}

	private void readTableDirectory () throws IOException {
		input.skip(4);
		int tableCount = input.readUnsignedShort();
		input.skip(6);

		byte[] tagBytes = new byte[4];
		for (int i = 0; i < tableCount; i++) {
			tagBytes[0] = input.readByte();
			tagBytes[1] = input.readByte();
			tagBytes[2] = input.readByte();
			tagBytes[3] = input.readByte();
			input.skip(4);
			int offset = (int)input.readUnsignedLong();
			input.skip(4);

			String tag = new String(tagBytes, "ISO-8859-1");
			if (tag.equals("head")) {
				headOffset = offset;
			} else if (tag.equals("kern")) {
				kernOffset = offset;
			} else if (tag.equals("GPOS")) {
				gposOffset = offset;
			}
		}
	}

	private void readHEAD (int fontSize) throws IOException {
		input.seek(headOffset + 2 * 4 + 2 * 4 + 2);
		int unitsPerEm = input.readUnsignedShort();
		scale = (float)fontSize / unitsPerEm;
	}

	private void readKERN () throws IOException {
		input.seek(kernOffset + 2);
		for (int subTableCount = input.readUnsignedShort(); subTableCount > 0; subTableCount--) {
			input.skip(2 * 2);
			int tupleIndex = input.readUnsignedShort();
			if (!((tupleIndex & 1) != 0) || (tupleIndex & 2) != 0 || (tupleIndex & 4) != 0) return;
			if (tupleIndex >> 8 != 0) continue;

			int kerningCount = input.readUnsignedShort();
			input.skip(3 * 2);
			while (kerningCount-- > 0) {
				int firstGlyphCode = input.readUnsignedShort();
				int secondGlyphCode = input.readUnsignedShort();
				int offset = (int)input.readShort();
				storeKerningOffset(firstGlyphCode, secondGlyphCode, offset);
			}
		}
	}

	private void readGPOS () throws IOException {
		// See https://www.microsoft.com/typography/otspec/gpos.htm for the format and semantics.
		// Useful tools are ttfdump and showttf.
		input.seek(gposOffset + 4 + 2 + 2);
		int lookupListOffset = input.readUnsignedShort();
		input.seek(gposOffset + lookupListOffset);

		int lookupListPosition = input.getPosition();
		int lookupCount = input.readUnsignedShort();
		int[] lookupOffsets = input.readUnsignedShortArray(lookupCount);

		for (int i = 0; i < lookupCount; i++) {
			int lookupPosition = lookupListPosition + lookupOffsets[i];
			input.seek(lookupPosition);
			int type = input.readUnsignedShort();
			readSubtables(type, lookupPosition);
		}
	}

	private void readSubtables (int type, int lookupPosition) throws IOException {
		input.skip(2);
		int subTableCount = input.readUnsignedShort();
		int[] subTableOffsets = input.readUnsignedShortArray(subTableCount);

		for (int i = 0; i < subTableCount; i++) {
			int subTablePosition = lookupPosition + subTableOffsets[i];
			readSubtable(type, subTablePosition);
		}
	}

	private void readSubtable (int type, int subTablePosition) throws IOException {
		input.seek(subTablePosition);
		if (type == 2) {
			readPairAdjustmentSubtable(subTablePosition);
		} else if (type == 9) {
			readExtensionPositioningSubtable(subTablePosition);
		}
	}

	private void readPairAdjustmentSubtable (int subTablePosition) throws IOException {
		int type = input.readUnsignedShort();
		if (type == 1) {
			readPairPositioningAdjustmentFormat1(subTablePosition);
		} else if (type == 2) {
			readPairPositioningAdjustmentFormat2(subTablePosition);
		}
	}

	private void readExtensionPositioningSubtable (int subTablePosition) throws IOException {
		int type = input.readUnsignedShort();
		if (type == 1) {
			readExtensionPositioningFormat1(subTablePosition);
		}
	}

	private void readPairPositioningAdjustmentFormat1 (long subTablePosition) throws IOException {
		int coverageOffset = input.readUnsignedShort();
		int valueFormat1 = input.readUnsignedShort();
		int valueFormat2 = input.readUnsignedShort();
		int pairSetCount = input.readUnsignedShort();
		int[] pairSetOffsets = input.readUnsignedShortArray(pairSetCount);

		input.seek((int)(subTablePosition + coverageOffset));
		int[] coverage = readCoverageTable();

		// The two should be equal, but just in case they're not, we can still do something sensible.
		pairSetCount = Math.min(pairSetCount, coverage.length);

		for (int i = 0; i < pairSetCount; i++) {
			int firstGlyph = coverage[i];
			input.seek((int)(subTablePosition + pairSetOffsets[i]));
			int pairValueCount = input.readUnsignedShort();
			for (int j = 0; j < pairValueCount; j++) {
				int secondGlyph = input.readUnsignedShort();
				int xAdvance1 = readXAdvanceFromValueRecord(valueFormat1);
				readXAdvanceFromValueRecord(valueFormat2); // Value2
				if (xAdvance1 != 0) {
					storeKerningOffset(firstGlyph, secondGlyph, xAdvance1);
				}
			}
		}
	}

	private void readPairPositioningAdjustmentFormat2 (int subTablePosition) throws IOException {
		int coverageOffset = input.readUnsignedShort();
		int valueFormat1 = input.readUnsignedShort();
		int valueFormat2 = input.readUnsignedShort();
		int classDefOffset1 = input.readUnsignedShort();
		int classDefOffset2 = input.readUnsignedShort();
		int class1Count = input.readUnsignedShort();
		int class2Count = input.readUnsignedShort();

		int position = input.getPosition();

		input.seek((int)(subTablePosition + coverageOffset));
		int[] coverage = readCoverageTable();

		input.seek(position);
		IntArray[] glyphsByClass1 = readClassDefinition(subTablePosition + classDefOffset1, class1Count);
		IntArray[] glyphsByClass2 = readClassDefinition(subTablePosition + classDefOffset2, class2Count);
		input.seek(position);

		for (int i = 0; i < coverage.length; i++) {
			int glyph = coverage[i];
			boolean found = false;
			for (int j = 1; j < class1Count && !found; j++) {
				found = glyphsByClass1[j].contains(glyph);
			}
			if (!found) {
				glyphsByClass1[0].add(glyph);
			}
		}

		for (int i = 0; i < class1Count; i++) {
			for (int j = 0; j < class2Count; j++) {
				int xAdvance1 = readXAdvanceFromValueRecord(valueFormat1);
				readXAdvanceFromValueRecord(valueFormat2); // Value2
				if (xAdvance1 == 0) continue;
				for (int k = 0; k < glyphsByClass1[i].size; k++) {
					int glyph1 = glyphsByClass1[i].items[k];
					for (int l = 0; l < glyphsByClass2[j].size; l++) {
						int glyph2 = glyphsByClass2[j].items[l];
						storeKerningOffset(glyph1, glyph2, xAdvance1);
					}
				}
			}
		}
	}

	private void readExtensionPositioningFormat1 (int subTablePosition) throws IOException {
		int lookupType = input.readUnsignedShort();
		int lookupPosition = subTablePosition + (int)input.readUnsignedLong();
		readSubtable(lookupType, lookupPosition);
	}

	private IntArray[] readClassDefinition (int position, int classCount) throws IOException {
		input.seek(position);

		IntArray[] glyphsByClass = new IntArray[classCount];
		for (int i = 0; i < classCount; i++) {
			glyphsByClass[i] = new IntArray();
		}

		int classFormat = input.readUnsignedShort();
		if (classFormat == 1) {
			readClassDefinitionFormat1(glyphsByClass);
		} else if (classFormat == 2) {
			readClassDefinitionFormat2(glyphsByClass);
		} else {
			throw new IOException("Unknown class definition table type " + classFormat);
		}
		return glyphsByClass;
	}

	private void readClassDefinitionFormat1 (IntArray[] glyphsByClass) throws IOException {
		int startGlyph = input.readUnsignedShort();
		int glyphCount = input.readUnsignedShort();
		int[] classValueArray = input.readUnsignedShortArray(glyphCount);
		for (int i = 0; i < glyphCount; i++) {
			int glyph = startGlyph + i;
			int glyphClass = classValueArray[i];
			if (glyphClass < glyphsByClass.length) {
				glyphsByClass[glyphClass].add(glyph);
			}
		}
	}

	private void readClassDefinitionFormat2 (IntArray[] glyphsByClass) throws IOException {
		int classRangeCount = input.readUnsignedShort();
		for (int i = 0; i < classRangeCount; i++) {
			int start = input.readUnsignedShort();
			int end = input.readUnsignedShort();
			int glyphClass = input.readUnsignedShort();
			if (glyphClass < glyphsByClass.length) {
				for (int glyph = start; glyph <= end; glyph++) {
					glyphsByClass[glyphClass].add(glyph);
				}
			}
		}
	}

	private int[] readCoverageTable () throws IOException {
		int format = input.readUnsignedShort();
		if (format == 1) {
			int glyphCount = input.readUnsignedShort();
			int[] glyphArray = input.readUnsignedShortArray(glyphCount);
			return glyphArray;
		} else if (format == 2) {
			int rangeCount = input.readUnsignedShort();
			IntArray glyphArray = new IntArray();
			for (int i = 0; i < rangeCount; i++) {
				int start = input.readUnsignedShort();
				int end = input.readUnsignedShort();
				input.skip(2);
				for (int glyph = start; glyph <= end; glyph++) {
					glyphArray.add(glyph);
				}
			}
			return glyphArray.shrink();
		}
		throw new IOException("Unknown coverage table format " + format);
	}

	private int readXAdvanceFromValueRecord (int valueFormat) throws IOException {
		int xAdvance = 0;
		for (int mask = 1; mask <= 0x8000 && mask <= valueFormat; mask <<= 1) {
			if ((valueFormat & mask) != 0) {
				int value = (int)input.readShort();
				if (mask == 0x0004) {
					xAdvance = value;
				}
			}
		}
		return xAdvance;
	}

	private static class TTFInputStream extends ByteArrayInputStream {
		public TTFInputStream (InputStream input) throws IOException {
			super(readAllBytes(input));
		}

		private static byte[] readAllBytes (InputStream input) throws IOException {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			int numRead;
			byte[] buffer = new byte[16384];
			while ((numRead = input.read(buffer, 0, buffer.length)) != -1) {
				out.write(buffer, 0, numRead);
			}
			return out.toByteArray();
		}

		public int getPosition () {
			return pos;
		}

		public void seek (int position) {
			pos = position;
		}

		public int readUnsignedByte () throws IOException {
			int b = read();
			if (b == -1) throw new EOFException("Unexpected end of file.");
			return b;
		}

		public byte readByte () throws IOException {
			return (byte)readUnsignedByte();
		}

		public int readUnsignedShort () throws IOException {
			return (readUnsignedByte() << 8) + readUnsignedByte();
		}

		public short readShort () throws IOException {
			return (short)readUnsignedShort();
		}

		public long readUnsignedLong () throws IOException {
			long value = readUnsignedByte();
			value = (value << 8) + readUnsignedByte();
			value = (value << 8) + readUnsignedByte();
			value = (value << 8) + readUnsignedByte();
			return value;
		}

		public int[] readUnsignedShortArray (int count) throws IOException {
			int[] shorts = new int[count];
			for (int i = 0; i < count; i++) {
				shorts[i] = readUnsignedShort();
			}
			return shorts;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy