• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
load(InputStream inputStream, int fontSize)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. */
getKernings()69 	public IntIntMap getKernings () {
70 		return kernings;
71 	}
72 
storeKerningOffset(int firstGlyphCode, int secondGlyphCode, int offset)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 
readTableDirectory()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 
readHEAD(int fontSize)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 
readKERN()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 
readGPOS()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 
readSubtables( int type, int lookupPosition)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 
readSubtable(int type, int subTablePosition)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 
readPairAdjustmentSubtable(int subTablePosition)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 
readExtensionPositioningSubtable(int subTablePosition)182 	private void readExtensionPositioningSubtable (int subTablePosition) throws IOException {
183 		int type = input.readUnsignedShort();
184 		if (type == 1) {
185 			readExtensionPositioningFormat1(subTablePosition);
186 		}
187 	}
188 
readPairPositioningAdjustmentFormat1(long subTablePosition)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 
readPairPositioningAdjustmentFormat2(int subTablePosition)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 
readExtensionPositioningFormat1(int subTablePosition)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 
readClassDefinition(int position, int classCount)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 
readClassDefinitionFormat1(IntArray[] glyphsByClass)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 
readClassDefinitionFormat2(IntArray[] glyphsByClass)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 
readCoverageTable()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 
readXAdvanceFromValueRecord(int valueFormat)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 {
TTFInputStream(InputStream input)351 		public TTFInputStream (InputStream input) throws IOException {
352 			super(readAllBytes(input));
353 		}
354 
readAllBytes(InputStream input)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 
getPosition()365 		public int getPosition () {
366 			return pos;
367 		}
368 
seek(int position)369 		public void seek (int position) {
370 			pos = position;
371 		}
372 
readUnsignedByte()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 
readByte()379 		public byte readByte () throws IOException {
380 			return (byte) readUnsignedByte();
381 		}
382 
readUnsignedShort()383 		public int readUnsignedShort () throws IOException {
384 			return (readUnsignedByte() << 8) + readUnsignedByte();
385 		}
386 
readShort()387 		public short readShort () throws IOException {
388 			return (short)readUnsignedShort();
389 		}
390 
readUnsignedLong()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 
readUnsignedShortArray(int count)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