Home | History | Annotate | Download | only in hiero
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.tools.hiero;
     18 
     19 import com.badlogic.gdx.utils.IntArray;
     20 import com.badlogic.gdx.utils.IntIntMap;
     21 
     22 import java.io.ByteArrayInputStream;
     23 import java.io.ByteArrayOutputStream;
     24 import java.io.EOFException;
     25 import java.io.IOException;
     26 import java.io.InputStream;
     27 
     28 /** Reads a TTF font file and provides access to kerning information.
     29  *
     30  * Thanks to the Apache FOP project for their inspiring work!
     31  *
     32  * @author Nathan Sweet */
     33 class Kerning {
     34 	private TTFInputStream input;
     35 	private float scale;
     36 	private int headOffset = -1;
     37 	private int kernOffset = -1;
     38 	private int gposOffset = -1;
     39 	private IntIntMap kernings = new IntIntMap();
     40 
     41 	/** @param inputStream The data for the TTF font.
     42 	 * @param fontSize The font size to use to determine kerning pixel offsets.
     43 	 * @throws IOException If the font could not be read. */
     44 	public void load (InputStream inputStream, int fontSize) throws IOException {
     45 		if (inputStream == null) throw new IllegalArgumentException("inputStream cannot be null.");
     46 		input = new TTFInputStream(inputStream);
     47 		inputStream.close();
     48 
     49 		readTableDirectory();
     50 		if (headOffset == -1) throw new IOException("HEAD table not found.");
     51 		readHEAD(fontSize);
     52 
     53 		// By reading the 'kern' table last, it takes precedence over the 'GPOS' table. We are more likely to interpret
     54 		// the GPOS table incorrectly because we ignore most of it, since BMFont doesn't support its features.
     55 		if (gposOffset != -1) {
     56 			input.seek(gposOffset);
     57 			readGPOS();
     58 		}
     59 		if (kernOffset != -1) {
     60 			input.seek(kernOffset);
     61 			readKERN();
     62 		}
     63 		input.close();
     64 		input = null;
     65 	}
     66 
     67 	/** @return A map from pairs of glyph codes to their kerning in pixels. Each map key encodes two glyph codes:
     68 	 * the high 16 bits form the first glyph code, and the low 16 bits form the second. */
     69 	public IntIntMap getKernings () {
     70 		return kernings;
     71 	}
     72 
     73 	private void storeKerningOffset (int firstGlyphCode, int secondGlyphCode, int offset) {
     74 		// Scale the offset values using the font size.
     75 		int value = Math.round(offset * scale);
     76 		if (value == 0) {
     77 			return;
     78 		}
     79 		int key = (firstGlyphCode << 16) | secondGlyphCode;
     80 		kernings.put(key, value);
     81 	}
     82 
     83 	private void readTableDirectory () throws IOException {
     84 		input.skip(4);
     85 		int tableCount = input.readUnsignedShort();
     86 		input.skip(6);
     87 
     88 		byte[] tagBytes = new byte[4];
     89 		for (int i = 0; i < tableCount; i++) {
     90 			tagBytes[0] = input.readByte();
     91 			tagBytes[1] = input.readByte();
     92 			tagBytes[2] = input.readByte();
     93 			tagBytes[3] = input.readByte();
     94 			input.skip(4);
     95 			int offset = (int) input.readUnsignedLong();
     96 			input.skip(4);
     97 
     98 			String tag = new String(tagBytes, "ISO-8859-1");
     99 			if (tag.equals("head")) {
    100 				headOffset = offset;
    101 			} else if (tag.equals("kern")) {
    102 				kernOffset = offset;
    103 			} else if (tag.equals("GPOS")) {
    104 				gposOffset = offset;
    105 			}
    106 		}
    107 	}
    108 
    109 	private void readHEAD (int fontSize) throws IOException {
    110 		input.seek(headOffset + 2 * 4 + 2 * 4 + 2);
    111 		int unitsPerEm = input.readUnsignedShort();
    112 		scale = (float)fontSize / unitsPerEm;
    113 	}
    114 
    115 	private void readKERN () throws IOException {
    116 		input.seek(kernOffset + 2);
    117 		for (int subTableCount = input.readUnsignedShort(); subTableCount > 0; subTableCount--) {
    118 			input.skip(2 * 2);
    119 			int tupleIndex = input.readUnsignedShort();
    120 			if (!((tupleIndex & 1) != 0) || (tupleIndex & 2) != 0 || (tupleIndex & 4) != 0) return;
    121 			if (tupleIndex >> 8 != 0) continue;
    122 
    123 			int kerningCount = input.readUnsignedShort();
    124 			input.skip(3 * 2);
    125 			while (kerningCount-- > 0) {
    126 				int firstGlyphCode = input.readUnsignedShort();
    127 				int secondGlyphCode = input.readUnsignedShort();
    128 				int offset = (int) input.readShort();
    129 				storeKerningOffset(firstGlyphCode, secondGlyphCode, offset);
    130 			}
    131 		}
    132 	}
    133 
    134 	private void readGPOS () throws IOException {
    135 		// See https://www.microsoft.com/typography/otspec/gpos.htm for the format and semantics.
    136 		// Useful tools are ttfdump and showttf.
    137 		input.seek(gposOffset + 4 + 2 + 2);
    138 		int lookupListOffset = input.readUnsignedShort();
    139 		input.seek(gposOffset + lookupListOffset);
    140 
    141 		int lookupListPosition = input.getPosition();
    142 		int lookupCount = input.readUnsignedShort();
    143 		int[] lookupOffsets = input.readUnsignedShortArray(lookupCount);
    144 
    145 		for (int i = 0; i < lookupCount; i++) {
    146 			int lookupPosition = lookupListPosition + lookupOffsets[i];
    147 			input.seek(lookupPosition);
    148 			int type = input.readUnsignedShort();
    149 			readSubtables(type, lookupPosition);
    150 		}
    151 	}
    152 
    153 	private void readSubtables ( int type, int lookupPosition) throws IOException {
    154 		input.skip(2);
    155 		int subTableCount = input.readUnsignedShort();
    156 		int[] subTableOffsets = input.readUnsignedShortArray(subTableCount);
    157 
    158 		for (int i = 0; i < subTableCount; i++) {
    159 			int subTablePosition = lookupPosition + subTableOffsets[i];
    160 			readSubtable(type, subTablePosition);
    161 		}
    162 	}
    163 
    164 	private void readSubtable (int type, int subTablePosition) throws IOException {
    165 		input.seek(subTablePosition);
    166 		if (type == 2) {
    167 			readPairAdjustmentSubtable(subTablePosition);
    168 		} else if (type == 9) {
    169 			readExtensionPositioningSubtable(subTablePosition);
    170 		}
    171 	}
    172 
    173 	private void readPairAdjustmentSubtable(int subTablePosition) throws IOException {
    174 		int type = input.readUnsignedShort();
    175 		if (type == 1) {
    176 			readPairPositioningAdjustmentFormat1(subTablePosition);
    177 		} else if (type == 2) {
    178 			readPairPositioningAdjustmentFormat2(subTablePosition);
    179 		}
    180 	}
    181 
    182 	private void readExtensionPositioningSubtable (int subTablePosition) throws IOException {
    183 		int type = input.readUnsignedShort();
    184 		if (type == 1) {
    185 			readExtensionPositioningFormat1(subTablePosition);
    186 		}
    187 	}
    188 
    189 	private void readPairPositioningAdjustmentFormat1 (long subTablePosition) throws IOException {
    190 		int coverageOffset = input.readUnsignedShort();
    191 		int valueFormat1 = input.readUnsignedShort();
    192 		int valueFormat2 = input.readUnsignedShort();
    193 		int pairSetCount = input.readUnsignedShort();
    194 		int[] pairSetOffsets = input.readUnsignedShortArray(pairSetCount);
    195 
    196 		input.seek((int) (subTablePosition + coverageOffset));
    197 		int[] coverage = readCoverageTable();
    198 
    199 		// The two should be equal, but just in case they're not, we can still do something sensible.
    200 		pairSetCount = Math.min(pairSetCount, coverage.length);
    201 
    202 		for (int i = 0; i < pairSetCount; i++) {
    203 			int firstGlyph = coverage[i];
    204 			input.seek((int) (subTablePosition + pairSetOffsets[i]));
    205 			int pairValueCount = input.readUnsignedShort();
    206 			for (int j = 0; j < pairValueCount; j++) {
    207 				int secondGlyph = input.readUnsignedShort();
    208 				int xAdvance1 = readXAdvanceFromValueRecord(valueFormat1);
    209 				readXAdvanceFromValueRecord(valueFormat2); // Value2
    210 				if (xAdvance1 != 0) {
    211 					storeKerningOffset(firstGlyph, secondGlyph, xAdvance1);
    212 				}
    213 			}
    214 		}
    215 	}
    216 
    217 	private void readPairPositioningAdjustmentFormat2 (int subTablePosition) throws IOException {
    218 		int coverageOffset = input.readUnsignedShort();
    219 		int valueFormat1 = input.readUnsignedShort();
    220 		int valueFormat2 = input.readUnsignedShort();
    221 		int classDefOffset1 = input.readUnsignedShort();
    222 		int classDefOffset2 = input.readUnsignedShort();
    223 		int class1Count = input.readUnsignedShort();
    224 		int class2Count = input.readUnsignedShort();
    225 
    226 		int position = input.getPosition();
    227 
    228 		input.seek((int) (subTablePosition + coverageOffset));
    229 		int[] coverage = readCoverageTable();
    230 
    231 		input.seek(position);
    232 		IntArray[] glyphsByClass1 = readClassDefinition(subTablePosition + classDefOffset1, class1Count);
    233 		IntArray[] glyphsByClass2 = readClassDefinition(subTablePosition + classDefOffset2, class2Count);
    234 		input.seek(position);
    235 
    236 		for (int i = 0; i < coverage.length; i++) {
    237 			int glyph = coverage[i];
    238 			boolean found = false;
    239 			for (int j = 1; j < class1Count && !found; j++) {
    240 				found = glyphsByClass1[j].contains(glyph);
    241 			}
    242 			if (!found) {
    243 				glyphsByClass1[0].add(glyph);
    244 			}
    245 		}
    246 
    247 		for (int i = 0; i < class1Count; i++) {
    248 			for (int j = 0; j < class2Count; j++) {
    249 				int xAdvance1 = readXAdvanceFromValueRecord(valueFormat1);
    250 				readXAdvanceFromValueRecord(valueFormat2); // Value2
    251 				if (xAdvance1 == 0) continue;
    252 				for (int k = 0; k < glyphsByClass1[i].size; k++) {
    253 					int glyph1 = glyphsByClass1[i].items[k];
    254 					for (int l = 0; l < glyphsByClass2[j].size; l++) {
    255 						int glyph2 = glyphsByClass2[j].items[l];
    256 						storeKerningOffset(glyph1, glyph2, xAdvance1);
    257 					}
    258 				}
    259 			}
    260 		}
    261 	}
    262 
    263 	private void readExtensionPositioningFormat1 (int subTablePosition) throws IOException {
    264 		int lookupType = input.readUnsignedShort();
    265 		int lookupPosition = subTablePosition + (int) input.readUnsignedLong();
    266 		readSubtable(lookupType, lookupPosition);
    267 	}
    268 
    269 	private IntArray[] readClassDefinition (int position, int classCount) throws IOException {
    270 		input.seek(position);
    271 
    272 		IntArray[] glyphsByClass = new IntArray[classCount];
    273 		for (int i = 0; i < classCount; i++) {
    274 			glyphsByClass[i] = new IntArray();
    275 		}
    276 
    277 		int classFormat = input.readUnsignedShort();
    278 		if (classFormat == 1) {
    279 			readClassDefinitionFormat1(glyphsByClass);
    280 		} else if (classFormat == 2) {
    281 			readClassDefinitionFormat2(glyphsByClass);
    282 		} else {
    283 			throw new IOException("Unknown class definition table type " + classFormat);
    284 		}
    285 		return glyphsByClass;
    286 	}
    287 
    288 	private void readClassDefinitionFormat1 (IntArray[] glyphsByClass) throws IOException {
    289 		int startGlyph = input.readUnsignedShort();
    290 		int glyphCount = input.readUnsignedShort();
    291 		int[] classValueArray = input.readUnsignedShortArray(glyphCount);
    292 		for (int i = 0; i < glyphCount; i++) {
    293 			int glyph = startGlyph + i;
    294 			int glyphClass = classValueArray[i];
    295 			if (glyphClass < glyphsByClass.length) {
    296 				glyphsByClass[glyphClass].add(glyph);
    297 			}
    298 		}
    299 	}
    300 
    301 	private void readClassDefinitionFormat2 (IntArray[] glyphsByClass) throws IOException {
    302 		int classRangeCount = input.readUnsignedShort();
    303 		for (int i = 0; i < classRangeCount; i++) {
    304 			int start = input.readUnsignedShort();
    305 			int end = input.readUnsignedShort();
    306 			int glyphClass = input.readUnsignedShort();
    307 			if (glyphClass < glyphsByClass.length) {
    308 				for (int glyph = start; glyph <= end; glyph++) {
    309 					glyphsByClass[glyphClass].add(glyph);
    310 				}
    311 			}
    312 		}
    313 	}
    314 
    315 	private int[] readCoverageTable () throws IOException {
    316 		int format = input.readUnsignedShort();
    317 		if (format == 1) {
    318 			int glyphCount = input.readUnsignedShort();
    319 			int[] glyphArray = input.readUnsignedShortArray(glyphCount);
    320 			return glyphArray;
    321 		} else if (format == 2) {
    322 			int rangeCount = input.readUnsignedShort();
    323 			IntArray glyphArray = new IntArray();
    324 			for (int i = 0; i < rangeCount; i++) {
    325 				int start = input.readUnsignedShort();
    326 				int end = input.readUnsignedShort();
    327 				input.skip(2);
    328 				for (int glyph = start; glyph <= end; glyph++) {
    329 					glyphArray.add(glyph);
    330 				}
    331 			}
    332 			return glyphArray.shrink();
    333 		}
    334 		throw new IOException("Unknown coverage table format " + format);
    335 	}
    336 
    337 	private int readXAdvanceFromValueRecord (int valueFormat) throws IOException {
    338 		int xAdvance = 0;
    339 		for (int mask = 1; mask <= 0x8000 && mask <= valueFormat; mask <<= 1) {
    340 			if ((valueFormat & mask) != 0) {
    341 				int value = (int) input.readShort();
    342 				if (mask == 0x0004) {
    343 					xAdvance = value;
    344 				}
    345 			}
    346 		}
    347 		return xAdvance;
    348 	}
    349 
    350 	private static class TTFInputStream extends ByteArrayInputStream {
    351 		public TTFInputStream (InputStream input) throws IOException {
    352 			super(readAllBytes(input));
    353 		}
    354 
    355 		private static byte[] readAllBytes(InputStream input) throws IOException {
    356 			ByteArrayOutputStream out = new ByteArrayOutputStream();
    357 			int numRead;
    358 			byte[] buffer = new byte[16384];
    359 			while ((numRead = input.read(buffer, 0, buffer.length)) != -1) {
    360 				out.write(buffer, 0, numRead);
    361 			}
    362 			return out.toByteArray();
    363 		}
    364 
    365 		public int getPosition () {
    366 			return pos;
    367 		}
    368 
    369 		public void seek (int position) {
    370 			pos = position;
    371 		}
    372 
    373 		public int readUnsignedByte () throws IOException {
    374 			int b = read();
    375 			if (b == -1) throw new EOFException("Unexpected end of file.");
    376 			return b;
    377 		}
    378 
    379 		public byte readByte () throws IOException {
    380 			return (byte) readUnsignedByte();
    381 		}
    382 
    383 		public int readUnsignedShort () throws IOException {
    384 			return (readUnsignedByte() << 8) + readUnsignedByte();
    385 		}
    386 
    387 		public short readShort () throws IOException {
    388 			return (short)readUnsignedShort();
    389 		}
    390 
    391 		public long readUnsignedLong () throws IOException {
    392 			long value = readUnsignedByte();
    393 			value = (value << 8) + readUnsignedByte();
    394 			value = (value << 8) + readUnsignedByte();
    395 			value = (value << 8) + readUnsignedByte();
    396 			return value;
    397 		}
    398 
    399 		public int[] readUnsignedShortArray (int count) throws IOException {
    400 			int[] shorts = new int[count];
    401 			for (int i = 0; i < count; i++) {
    402 				shorts[i] = readUnsignedShort();
    403 			}
    404 			return shorts;
    405 		}
    406 	}
    407 }
    408