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