1 /* 2 * Copyright 2010 Google Inc. All Rights Reserved. 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.google.typography.font.sfntly; 18 19 import com.google.typography.font.sfntly.data.FontInputStream; 20 import com.google.typography.font.sfntly.data.FontOutputStream; 21 import com.google.typography.font.sfntly.data.ReadableFontData; 22 import com.google.typography.font.sfntly.data.WritableFontData; 23 import com.google.typography.font.sfntly.math.Fixed1616; 24 import com.google.typography.font.sfntly.math.FontMath; 25 import com.google.typography.font.sfntly.table.FontDataTable; 26 import com.google.typography.font.sfntly.table.Header; 27 import com.google.typography.font.sfntly.table.Table; 28 import com.google.typography.font.sfntly.table.core.CMapTable; 29 import com.google.typography.font.sfntly.table.core.FontHeaderTable; 30 import com.google.typography.font.sfntly.table.core.HorizontalDeviceMetricsTable; 31 import com.google.typography.font.sfntly.table.core.HorizontalHeaderTable; 32 import com.google.typography.font.sfntly.table.core.HorizontalMetricsTable; 33 import com.google.typography.font.sfntly.table.core.MaximumProfileTable; 34 import com.google.typography.font.sfntly.table.core.NameTable; 35 import com.google.typography.font.sfntly.table.truetype.LocaTable; 36 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.HashMap; 44 import java.util.Iterator; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.SortedSet; 49 import java.util.TreeMap; 50 import java.util.TreeSet; 51 import java.util.logging.Logger; 52 53 /** 54 * An sfnt container font object. This object is immutable and thread safe. To 55 * construct one use an instance of {@link Font.Builder}. 56 * 57 * @author Stuart Gill 58 */ 59 public class Font { 60 61 private static final Logger logger = 62 Logger.getLogger(Font.class.getCanonicalName()); 63 64 /** 65 * Offsets to specific elements in the underlying data. These offsets are relative to the 66 * start of the table or the start of sub-blocks within the table. 67 */ 68 private enum Offset { 69 // Offsets within the main directory 70 sfntVersion(0), 71 numTables(4), 72 searchRange(6), 73 entrySelector(8), 74 rangeShift(10), 75 tableRecordBegin(12), 76 sfntHeaderSize(12), 77 78 // Offsets within a specific table record 79 tableTag(0), 80 tableCheckSum(4), 81 tableOffset(8), 82 tableLength(12), 83 tableRecordSize(16); 84 85 private final int offset; 86 Offset(int offset)87 private Offset(int offset) { 88 this.offset = offset; 89 } 90 } 91 92 /** 93 * Ordering of tables for different font types. 94 */ 95 private final static List<Integer> CFF_TABLE_ORDERING; 96 private final static List<Integer> TRUE_TYPE_TABLE_ORDERING; 97 static { 98 Integer[] cffArray = new Integer[] {Tag.head, 99 Tag.hhea, 100 Tag.maxp, 101 Tag.OS_2, 102 Tag.name, 103 Tag.cmap, 104 Tag.post, 105 Tag.CFF}; 106 List<Integer> cffList = new ArrayList<Integer>(cffArray.length); Collections.addAll(cffList, cffArray)107 Collections.addAll(cffList, cffArray); 108 CFF_TABLE_ORDERING = Collections.unmodifiableList(cffList); 109 110 Integer[] ttArray = new Integer[] {Tag.head, 111 Tag.hhea, 112 Tag.maxp, 113 Tag.OS_2, 114 Tag.hmtx, 115 Tag.LTSH, 116 Tag.VDMX, 117 Tag.hdmx, 118 Tag.cmap, 119 Tag.fpgm, 120 Tag.prep, 121 Tag.cvt, 122 Tag.loca, 123 Tag.glyf, 124 Tag.kern, 125 Tag.name, 126 Tag.post, 127 Tag.gasp, 128 Tag.PCLT, 129 Tag.DSIG}; 130 List<Integer> ttList = new ArrayList<Integer>(ttArray.length); Collections.addAll(ttList, ttArray)131 Collections.addAll(ttList, ttArray); 132 TRUE_TYPE_TABLE_ORDERING = Collections.unmodifiableList(ttList); 133 } 134 135 /** 136 * Platform ids. These are used in a number of places within the font whenever 137 * the platform needs to be specified. 138 * 139 * @see NameTable 140 * @see CMapTable 141 */ 142 public enum PlatformId { 143 Unknown(-1), Unicode(0), Macintosh(1), ISO(2), Windows(3), Custom(4); 144 145 private final int value; 146 PlatformId(int value)147 private PlatformId(int value) { 148 this.value = value; 149 } 150 value()151 public int value() { 152 return this.value; 153 } 154 equals(int value)155 public boolean equals(int value) { 156 return value == this.value; 157 } 158 valueOf(int value)159 public static PlatformId valueOf(int value) { 160 for (PlatformId platform : PlatformId.values()) { 161 if (platform.equals(value)) { 162 return platform; 163 } 164 } 165 return Unknown; 166 } 167 } 168 169 /** 170 * Unicode encoding ids. These are used in a number of places within the font 171 * whenever character encodings need to be specified. 172 * 173 * @see NameTable 174 * @see CMapTable 175 */ 176 public enum UnicodeEncodingId { 177 // Unicode Platform Encodings 178 Unknown(-1), 179 Unicode1_0(0), 180 Unicode1_1(1), 181 ISO10646(2), 182 Unicode2_0_BMP(3), 183 Unicode2_0(4), 184 UnicodeVariationSequences(5); 185 186 private final int value; 187 UnicodeEncodingId(int value)188 private UnicodeEncodingId(int value) { 189 this.value = value; 190 } 191 value()192 public int value() { 193 return this.value; 194 } 195 equals(int value)196 public boolean equals(int value) { 197 return value == this.value; 198 } 199 valueOf(int value)200 public static UnicodeEncodingId valueOf(int value) { 201 for (UnicodeEncodingId encoding : UnicodeEncodingId.values()) { 202 if (encoding.equals(value)) { 203 return encoding; 204 } 205 } 206 return Unknown; 207 } 208 } 209 210 /** 211 * Windows encoding ids. These are used in a number of places within the font 212 * whenever character encodings need to be specified. 213 * 214 * @see NameTable 215 * @see CMapTable 216 */ 217 public enum WindowsEncodingId { 218 // Windows Platform Encodings 219 Unknown(-1), 220 Symbol(0), 221 UnicodeUCS2(1), 222 ShiftJIS(2), 223 PRC(3), 224 Big5(4), 225 Wansung(5), 226 Johab(6), 227 UnicodeUCS4(10); 228 229 private final int value; 230 WindowsEncodingId(int value)231 private WindowsEncodingId(int value) { 232 this.value = value; 233 } 234 value()235 public int value() { 236 return this.value; 237 } 238 equals(int value)239 public boolean equals(int value) { 240 return value == this.value; 241 } 242 valueOf(int value)243 public static WindowsEncodingId valueOf(int value) { 244 for (WindowsEncodingId encoding : WindowsEncodingId.values()) { 245 if (encoding.equals(value)) { 246 return encoding; 247 } 248 } 249 return Unknown; 250 } 251 } 252 253 /** 254 * Macintosh encoding ids. These are used in a number of places within the 255 * font whenever character encodings need to be specified. 256 * 257 * @see NameTable 258 * @see CMapTable 259 */ 260 public enum MacintoshEncodingId { 261 // Macintosh Platform Encodings 262 Unknown(-1), 263 Roman(0), 264 Japanese(1), 265 ChineseTraditional(2), 266 Korean(3), 267 Arabic(4), 268 Hebrew(5), 269 Greek(6), 270 Russian(7), 271 RSymbol(8), 272 Devanagari(9), 273 Gurmukhi(10), 274 Gujarati(11), 275 Oriya(12), 276 Bengali(13), 277 Tamil(14), 278 Telugu(15), 279 Kannada(16), 280 Malayalam(17), 281 Sinhalese(18), 282 Burmese(19), 283 Khmer(20), 284 Thai(21), 285 Laotian(22), 286 Georgian(23), 287 Armenian(24), 288 ChineseSimplified(25), 289 Tibetan(26), 290 Mongolian(27), 291 Geez(28), 292 Slavic(29), 293 Vietnamese(30), 294 Sindhi(31), 295 Uninterpreted(32); 296 297 private final int value; 298 MacintoshEncodingId(int value)299 private MacintoshEncodingId(int value) { 300 this.value = value; 301 } 302 value()303 public int value() { 304 return this.value; 305 } 306 equals(int value)307 public boolean equals(int value) { 308 return value == this.value; 309 } 310 valueOf(int value)311 public static MacintoshEncodingId valueOf(int value) { 312 for (MacintoshEncodingId encoding : MacintoshEncodingId.values()) { 313 if (encoding.equals(value)) { 314 return encoding; 315 } 316 } 317 return Unknown; 318 } 319 } 320 321 public static final int SFNTVERSION_1 = Fixed1616.fixed(1, 0); 322 323 private final int sfntVersion; 324 private final byte[] digest; 325 private long checksum; 326 327 private Map<Integer, ? extends Table> tables; // these get set in the builder 328 329 /** 330 * Constructor. 331 * 332 * @param sfntVersion the sfnt version 333 * @param digest the computed digest for the font; null if digest was not 334 * computed 335 */ Font(int sfntVersion, byte[] digest)336 private Font(int sfntVersion, byte[] digest) { 337 this.sfntVersion = sfntVersion; 338 this.digest = digest; 339 } 340 341 /** 342 * Gets the sfnt version set in the sfnt wrapper of the font. 343 * 344 * @return the sfnt version 345 */ sfntVersion()346 public int sfntVersion() { 347 return this.sfntVersion; 348 } 349 350 /** 351 * Gets a copy of the fonts digest that was created when the font was read. If 352 * no digest was set at creation time then the return result will be null. 353 * 354 * @return a copy of the digest array or <code>null</code> if one wasn't set 355 * at creation time 356 */ digest()357 public byte[] digest() { 358 if (this.digest == null) { 359 return null; 360 } 361 return Arrays.copyOf(this.digest, this.digest.length); 362 } 363 364 /** 365 * Get the checksum for this font. 366 * 367 * @return the font checksum 368 */ checksum()369 public long checksum() { 370 return this.checksum; 371 } 372 373 /** 374 * Get the number of tables in this font. 375 * 376 * @return the number of tables 377 */ numTables()378 public int numTables() { 379 return this.tables.size(); 380 } 381 382 /** 383 * Get an iterator over all the tables in the font. 384 * 385 * @return a table iterator 386 */ iterator()387 public Iterator<? extends Table> iterator() { 388 return this.tables.values().iterator(); 389 } 390 391 /** 392 * Does the font have a particular table. 393 * 394 * @param tag the table identifier 395 * @return true if the table is in the font; false otherwise 396 */ hasTable(int tag)397 public boolean hasTable(int tag) { 398 return this.tables.containsKey(tag); 399 } 400 401 /** 402 * Get the table in this font with the specified id. 403 * 404 * @param <T> the type of the table 405 * @param tag the identifier of the table 406 * @return the table specified if it exists; null otherwise 407 */ 408 @SuppressWarnings("unchecked") getTable(int tag)409 public <T extends Table> T getTable(int tag) { 410 return (T) this.tables.get(tag); 411 } 412 413 /** 414 * Get a map of the tables in this font accessed by table tag. 415 * 416 * @return an unmodifiable view of the tables in this font 417 */ tableMap()418 public Map<Integer, ? extends Table> tableMap() { 419 return Collections.unmodifiableMap(this.tables); 420 } 421 422 @Override toString()423 public String toString() { 424 StringBuilder sb = new StringBuilder(); 425 sb.append("digest = "); 426 byte[] digest = this.digest(); 427 if (digest != null) { 428 for (int i = 0; i < digest.length; i++) { 429 int d = 0xff & digest[i]; 430 if (d < 0x10) { 431 sb.append("0"); 432 } 433 sb.append(Integer.toHexString(d)); 434 } 435 } 436 sb.append("\n["); 437 sb.append(Fixed1616.toString(sfntVersion)); 438 sb.append(", "); 439 sb.append(this.numTables()); 440 sb.append("]\n"); 441 Iterator<? extends Table> iter = this.iterator(); 442 while (iter.hasNext()) { 443 FontDataTable table = iter.next(); 444 sb.append("\t"); 445 sb.append(table); 446 sb.append("\n"); 447 } 448 return sb.toString(); 449 } 450 451 /** 452 * Serialize the font to the output stream. 453 * 454 * @param os the destination for the font serialization 455 * @param tableOrdering the table ordering to apply 456 * @throws IOException 457 */ serialize(OutputStream os, List<Integer> tableOrdering)458 void serialize(OutputStream os, List<Integer> tableOrdering) throws IOException { 459 List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering); 460 List<Header> tableRecords = buildTableHeadersForSerialization(finalTableOrdering); 461 FontOutputStream fos = new FontOutputStream(os); 462 this.serializeHeader(fos, tableRecords); 463 this.serializeTables(fos, tableRecords); 464 } 465 466 /** 467 * Build the table headers to be used for serialization. These headers will be 468 * filled out with the data required for serialization. The headers will be 469 * sorted in the order specified and only those specified will have headers 470 * generated. 471 * 472 * @param tableOrdering the tables to generate headers for and the order to 473 * sort them 474 * @return a list of table headers ready for serialization 475 */ buildTableHeadersForSerialization(List<Integer> tableOrdering)476 private List<Header> buildTableHeadersForSerialization(List<Integer> tableOrdering) { 477 List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering); 478 479 List<Header> tableHeaders = new ArrayList<Header>(this.numTables()); 480 int tableOffset = 481 Offset.tableRecordBegin.offset + this.numTables() * Offset.tableRecordSize.offset; 482 for (Integer tag : finalTableOrdering) { 483 Table table = this.tables.get(tag); 484 if (table != null) { 485 tableHeaders.add(new Header( 486 tag, table.calculatedChecksum(), tableOffset, table.header().length())); 487 // write on boundary of 4 bytes 488 tableOffset += (table.dataLength() + 3) & ~3; 489 } 490 } 491 return tableHeaders; 492 } 493 494 /** 495 * Searialize the headers. 496 * 497 * @param fos the destination stream for the headers 498 * @param tableHeaders the headers to serialize 499 * @throws IOException 500 */ serializeHeader(FontOutputStream fos, List<Header> tableHeaders)501 private void serializeHeader(FontOutputStream fos, List<Header> tableHeaders) 502 throws IOException { 503 fos.writeFixed(this.sfntVersion); 504 fos.writeUShort(tableHeaders.size()); 505 int log2OfMaxPowerOf2 = FontMath.log2(tableHeaders.size()); 506 int searchRange = 2 << (log2OfMaxPowerOf2 - 1 + 4); 507 fos.writeUShort(searchRange); 508 fos.writeUShort(log2OfMaxPowerOf2); 509 fos.writeUShort((tableHeaders.size() * 16) - searchRange); 510 511 List<Header> sortedHeaders = new ArrayList<Header>(tableHeaders); 512 Collections.sort(sortedHeaders, Header.COMPARATOR_BY_TAG); 513 514 for (Header record : sortedHeaders) { 515 fos.writeULong(record.tag()); 516 fos.writeULong(record.checksum()); 517 fos.writeULong(record.offset()); 518 fos.writeULong(record.length()); 519 } 520 } 521 522 /** 523 * Serialize the tables. 524 * 525 * @param fos the destination stream for the headers 526 * @param tableHeaders the headers for the tables to serialize 527 * @throws IOException 528 */ serializeTables(FontOutputStream fos, List<Header> tableHeaders)529 private void serializeTables(FontOutputStream fos, List<Header> tableHeaders) 530 throws IOException { 531 532 for (Header record : tableHeaders) { 533 Table table = this.getTable(record.tag()); 534 if (table == null) { 535 throw new IOException("Table out of sync with font header."); 536 } 537 int tableSize = table.serialize(fos); 538 int fillerSize = ((tableSize + 3) & ~3) - tableSize; 539 for (int i = 0; i < fillerSize; i++) { 540 fos.write(0); 541 } 542 } 543 } 544 545 /** 546 * Generate the full table ordering to used for serialization. The full 547 * ordering uses the partial ordering as a seed and then adds all remaining 548 * tables in the font in an undefined order. 549 * 550 * @param defaultTableOrdering the partial ordering to be used as a seed for 551 * the full ordering 552 * @return the full ordering for serialization 553 */ generateTableOrdering(List<Integer> defaultTableOrdering)554 private List<Integer> generateTableOrdering(List<Integer> defaultTableOrdering) { 555 List<Integer> tableOrdering = new ArrayList<Integer>(this.tables.size()); 556 if (defaultTableOrdering == null) { 557 defaultTableOrdering = defaultTableOrdering(); 558 } 559 560 Set<Integer> tablesInFont = new TreeSet<Integer>(this.tables.keySet()); 561 562 // add all the default ordering 563 for (Integer tag : defaultTableOrdering) { 564 if (this.hasTable(tag)) { 565 tableOrdering.add(tag); 566 tablesInFont.remove(tag); 567 } 568 } 569 570 // add all the rest 571 for (Integer tag : tablesInFont) { 572 tableOrdering.add(tag); 573 } 574 575 return tableOrdering; 576 } 577 578 /** 579 * Get the default table ordering based on the type of the font. 580 * 581 * @return the default table ordering 582 */ defaultTableOrdering()583 private List<Integer> defaultTableOrdering() { 584 if (this.hasTable(Tag.CFF)) { 585 return Font.CFF_TABLE_ORDERING; 586 } 587 return Font.TRUE_TYPE_TABLE_ORDERING; 588 } 589 590 /** 591 * A builder for a font object. The builder allows the for the creation of 592 * immutable {@link Font} objects. The builder is a one use non-thread safe 593 * object and cnce the {@link Font} object has been created it is no longer 594 * usable. To create a further {@link Font} object new builder will be 595 * required. 596 * 597 * @author Stuart Gill 598 * 599 */ 600 public static final class Builder { 601 602 private Map<Integer, Table.Builder<? extends Table>> tableBuilders; 603 private FontFactory factory; 604 private int sfntVersion = SFNTVERSION_1; 605 private int numTables; 606 @SuppressWarnings("unused") 607 private int searchRange; 608 @SuppressWarnings("unused") 609 private int entrySelector; 610 @SuppressWarnings("unused") 611 private int rangeShift; 612 private Map<Header, WritableFontData> dataBlocks; 613 private byte[] digest; 614 Builder(FontFactory factory)615 private Builder(FontFactory factory) { 616 this.factory = factory; 617 this.tableBuilders = new HashMap<Integer, Table.Builder<? extends Table>>(); 618 } 619 loadFont(InputStream is)620 private void loadFont(InputStream is) throws IOException { 621 if (is == null) { 622 throw new IOException("No input stream for font."); 623 } 624 FontInputStream fontIS = null; 625 try { 626 fontIS = new FontInputStream(is); 627 SortedSet<Header> records = readHeader(fontIS); 628 this.dataBlocks = loadTableData(records, fontIS); 629 this.tableBuilders = buildAllTableBuilders(this.dataBlocks); 630 } finally { 631 fontIS.close(); 632 } 633 } 634 loadFont(WritableFontData wfd, int offsetToOffsetTable)635 private void loadFont(WritableFontData wfd, int offsetToOffsetTable) throws IOException { 636 if (wfd == null) { 637 throw new IOException("No data for font."); 638 } 639 SortedSet<Header> records = readHeader(wfd, offsetToOffsetTable); 640 this.dataBlocks = loadTableData(records, wfd); 641 this.tableBuilders = buildAllTableBuilders(this.dataBlocks); 642 } 643 644 static final Builder getOTFBuilder(FontFactory factory, InputStream is)645 getOTFBuilder(FontFactory factory, InputStream is) throws IOException { 646 Builder builder = new Builder(factory); 647 builder.loadFont(is); 648 return builder; 649 } 650 getOTFBuilder( FontFactory factory, WritableFontData wfd, int offsetToOffsetTable)651 static final Builder getOTFBuilder( 652 FontFactory factory, WritableFontData wfd, int offsetToOffsetTable) throws IOException { 653 Builder builder = new Builder(factory); 654 builder.loadFont(wfd, offsetToOffsetTable); 655 return builder; 656 } 657 getOTFBuilder(FontFactory factory)658 static final Builder getOTFBuilder(FontFactory factory) { 659 return new Builder(factory); 660 } 661 662 /** 663 * Get the font factory that created this font builder. 664 * 665 * @return the font factory 666 */ getFontFactory()667 public FontFactory getFontFactory() { 668 return this.factory; 669 } 670 671 /** 672 * Is the font ready to build? 673 * 674 * @return true if ready to build; false otherwise 675 */ readyToBuild()676 public boolean readyToBuild() { 677 // just read in data with no manipulation 678 if (this.tableBuilders == null && this.dataBlocks != null && this.dataBlocks.size() > 0) { 679 return true; 680 } 681 682 for (Table.Builder<? extends Table> tableBuilder : this.tableBuilders.values()) { 683 if (tableBuilder.readyToBuild() == false) { 684 return false; 685 } 686 } 687 return true; 688 } 689 690 /** 691 * Build the {@link Font}. After this call this builder will no longer be 692 * usable. 693 * 694 * @return a {@link Font} 695 */ build()696 public Font build() { 697 Map<Integer, ? extends Table> tables = null; 698 699 Font font = new Font(this.sfntVersion, this.digest); 700 701 if (this.tableBuilders.size() > 0) { 702 tables = buildTablesFromBuilders(font, this.tableBuilders); 703 } 704 font.tables = tables; 705 this.tableBuilders = null; 706 this.dataBlocks = null; 707 return font; 708 } 709 710 /** 711 * Set a unique fingerprint for the font object. 712 * 713 * @param digest a unique identifier for the font 714 */ setDigest(byte[] digest)715 public void setDigest(byte[] digest) { 716 this.digest = digest; 717 } 718 719 /** 720 * Clear all table builders. 721 */ clearTableBuilders()722 public void clearTableBuilders() { 723 this.tableBuilders.clear(); 724 } 725 726 /** 727 * Does this font builder have the specified table builder. 728 * 729 * @param tag the table builder tag 730 * @return true if there is a builder for that table; false otherwise 731 */ hasTableBuilder(int tag)732 public boolean hasTableBuilder(int tag) { 733 return this.tableBuilders.containsKey(tag); 734 } 735 736 /** 737 * Get the table builder for the given tag. If there is no builder for that 738 * tag then return a null. 739 * 740 * @param tag the table builder tag 741 * @return the builder for the tag; null if there is no builder for that tag 742 */ getTableBuilder(int tag)743 public Table.Builder<? extends Table> getTableBuilder(int tag) { 744 Table.Builder<? extends Table> builder = this.tableBuilders.get(tag); 745 return builder; 746 } 747 748 /** 749 * Creates a new empty table builder for the table type given by the table 750 * id tag. 751 * 752 * This new table will be added to the font and will replace any existing 753 * builder for that table. 754 * 755 * @param tag 756 * @return new empty table of the type specified by tag; if tag is not known 757 * then a generic OpenTypeTable is returned 758 */ newTableBuilder(int tag)759 public Table.Builder<? extends Table> newTableBuilder(int tag) { 760 Header header = new Header(tag); 761 Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, null); 762 this.tableBuilders.put(header.tag(), builder); 763 764 return builder; 765 } 766 767 /** 768 * Creates a new table builder for the table type given by the table id tag. 769 * It makes a copy of the data provided and uses that copy for the table. 770 * 771 * This new table has been added to the font and will replace any existing 772 * builder for that table. 773 * 774 * @param tag 775 * @param srcData 776 * @return new empty table of the type specified by tag; if tag is not known 777 * then a generic OpenTypeTable is returned 778 */ newTableBuilder(int tag, ReadableFontData srcData)779 public Table.Builder<? extends Table> newTableBuilder(int tag, ReadableFontData srcData) { 780 WritableFontData data; 781 data = WritableFontData.createWritableFontData(srcData.length()); 782 // TODO(stuartg): take over original data instead? 783 srcData.copyTo(data); 784 785 Header header = new Header(tag, data.length()); 786 Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data); 787 788 this.tableBuilders.put(tag, builder); 789 790 return builder; 791 } 792 793 /** 794 * Get a map of the table builders in this font builder accessed by table 795 * tag. 796 * 797 * @return an unmodifiable view of the table builders in this font builder 798 */ tableBuilderMap()799 public Map<Integer, Table.Builder<? extends Table>> tableBuilderMap() { 800 return Collections.unmodifiableMap(this.tableBuilders); 801 } 802 803 /** 804 * Remove the specified table builder from the font builder. 805 * 806 * @param tag the table builder to remove 807 * @return the table builder removed 808 */ removeTableBuilder(int tag)809 public Table.Builder<? extends Table> removeTableBuilder(int tag) { 810 return this.tableBuilders.remove(tag); 811 } 812 813 /** 814 * Get the number of table builders in the font builder. 815 * 816 * @return the number of table builders 817 */ tableBuilderCount()818 public int tableBuilderCount() { 819 return this.tableBuilders.size(); 820 } 821 822 @SuppressWarnings("unused") sfntWrapperSize()823 private int sfntWrapperSize() { 824 return Offset.sfntHeaderSize.offset + 825 (Offset.tableRecordSize.offset * this.tableBuilders.size()); 826 } 827 buildAllTableBuilders( Map<Header, WritableFontData> tableData)828 private Map<Integer, Table.Builder<? extends Table>> buildAllTableBuilders( 829 Map<Header, WritableFontData> tableData) { 830 Map<Integer, Table.Builder<? extends Table>> builderMap = 831 new HashMap<Integer, Table.Builder<? extends Table>>(); 832 Set<Header> records = tableData.keySet(); 833 for (Header record : records) { 834 Table.Builder<? extends Table> builder = getTableBuilder(record, tableData.get(record)); 835 builderMap.put(record.tag(), builder); 836 } 837 interRelateBuilders(builderMap); 838 return builderMap; 839 } 840 getTableBuilder(Header header, WritableFontData data)841 private Table.Builder<? extends Table> getTableBuilder(Header header, WritableFontData data) { 842 Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data); 843 return builder; 844 } 845 buildTablesFromBuilders(Font font, Map<Integer, Table.Builder<? extends Table>> builderMap)846 private static Map<Integer, Table> buildTablesFromBuilders(Font font, 847 Map<Integer, Table.Builder<? extends Table>> builderMap) { 848 Map<Integer, Table> tableMap = new TreeMap<Integer, Table>(); 849 850 interRelateBuilders(builderMap); 851 852 long fontChecksum = 0; 853 boolean tablesChanged = false; 854 FontHeaderTable.Builder headerTableBuilder = null; 855 856 // now build all the tables 857 for (Table.Builder<? extends Table> builder : builderMap.values()) { 858 Table table = null; 859 if (Tag.isHeaderTable(builder.header().tag())) { 860 headerTableBuilder = (FontHeaderTable.Builder) builder; 861 continue; 862 } 863 if (builder.readyToBuild()) { 864 tablesChanged |= builder.changed(); 865 table = builder.build(); 866 } 867 if (table == null) { 868 throw new RuntimeException("Unable to build table - " + builder); 869 } 870 long tableChecksum = table.calculatedChecksum(); 871 fontChecksum += tableChecksum; 872 tableMap.put(table.header().tag(), table); 873 } 874 875 // now fix up the header table 876 Table headerTable = null; 877 if (headerTableBuilder != null) { 878 if (tablesChanged) { 879 headerTableBuilder.setFontChecksum(fontChecksum); 880 } 881 if (headerTableBuilder.readyToBuild()) { 882 tablesChanged |= headerTableBuilder.changed(); 883 headerTable = headerTableBuilder.build(); 884 } 885 if (headerTable == null) { 886 throw new RuntimeException("Unable to build table - " + headerTableBuilder); 887 } 888 fontChecksum += headerTable.calculatedChecksum(); 889 tableMap.put(headerTable.header().tag(), headerTable); 890 } 891 892 font.checksum = fontChecksum & 0xffffffffL; 893 return tableMap; 894 } 895 896 private static void interRelateBuilders(Map<Integer, Table.Builder<? extends Table>> builderMap)897 interRelateBuilders(Map<Integer, Table.Builder<? extends Table>> builderMap) { 898 FontHeaderTable.Builder headerTableBuilder = 899 (FontHeaderTable.Builder) builderMap.get(Tag.head); 900 HorizontalHeaderTable.Builder horizontalHeaderBuilder = 901 (HorizontalHeaderTable.Builder) builderMap.get(Tag.hhea); 902 MaximumProfileTable.Builder maxProfileBuilder = 903 (MaximumProfileTable.Builder) builderMap.get(Tag.maxp); 904 LocaTable.Builder locaTableBuilder = 905 (LocaTable.Builder) builderMap.get(Tag.loca); 906 HorizontalMetricsTable.Builder horizontalMetricsBuilder = 907 (HorizontalMetricsTable.Builder) builderMap.get(Tag.hmtx); 908 HorizontalDeviceMetricsTable.Builder hdmxTableBuilder = 909 (HorizontalDeviceMetricsTable.Builder) builderMap.get(Tag.hdmx); 910 911 // set the inter table data required to build certain tables 912 if (horizontalMetricsBuilder != null) { 913 if (maxProfileBuilder != null) { 914 horizontalMetricsBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs()); 915 } 916 if (horizontalHeaderBuilder != null) { 917 horizontalMetricsBuilder.setNumberOfHMetrics( 918 horizontalHeaderBuilder.numberOfHMetrics()); 919 } 920 } 921 922 if (locaTableBuilder != null) { 923 if (maxProfileBuilder != null) { 924 locaTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs()); 925 } 926 if (headerTableBuilder != null) { 927 locaTableBuilder.setFormatVersion(headerTableBuilder.indexToLocFormat()); 928 } 929 } 930 931 if (hdmxTableBuilder != null) { 932 if (maxProfileBuilder != null) { 933 hdmxTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs()); 934 } 935 } 936 } 937 readHeader(FontInputStream is)938 private SortedSet<Header> readHeader(FontInputStream is) throws IOException { 939 SortedSet<Header> records = 940 new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET); 941 942 this.sfntVersion = is.readFixed(); 943 this.numTables = is.readUShort(); 944 this.searchRange = is.readUShort(); 945 this.entrySelector = is.readUShort(); 946 this.rangeShift = is.readUShort(); 947 948 for (int tableNumber = 0; tableNumber < this.numTables; tableNumber++) { 949 Header table = new Header(is.readULongAsInt(), // safe since the tag is ASCII 950 is.readULong(), // checksum 951 is.readULongAsInt(), // offset 952 is.readULongAsInt()); // length 953 records.add(table); 954 } 955 return records; 956 } 957 loadTableData( SortedSet<Header> headers, FontInputStream is)958 private Map<Header, WritableFontData> loadTableData( 959 SortedSet<Header> headers, FontInputStream is) throws IOException { 960 Map<Header, WritableFontData> tableData = 961 new HashMap<Header, WritableFontData>(headers.size()); 962 logger.fine("######## Reading Table Data"); 963 for (Header tableHeader : headers) { 964 is.skip(tableHeader.offset() - is.position()); 965 logger.finer("\t" + tableHeader); 966 logger.finest("\t\tStream Position = " + Integer.toHexString((int) is.position())); 967 // don't close this or the whole stream is gone 968 FontInputStream tableIS = new FontInputStream(is, tableHeader.length()); 969 // TODO(stuartg): start tracking bad tables and other errors 970 WritableFontData data = WritableFontData.createWritableFontData(tableHeader.length()); 971 data.copyFrom(tableIS, tableHeader.length()); 972 tableData.put(tableHeader, data); 973 } 974 return tableData; 975 } 976 readHeader(ReadableFontData fd, int offset)977 private SortedSet<Header> readHeader(ReadableFontData fd, int offset) { 978 SortedSet<Header> records = 979 new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET); 980 981 this.sfntVersion = fd.readFixed(offset + Offset.sfntVersion.offset); 982 this.numTables = fd.readUShort(offset + Offset.numTables.offset); 983 this.searchRange = fd.readUShort(offset + Offset.searchRange.offset); 984 this.entrySelector = fd.readUShort(offset + Offset.entrySelector.offset); 985 this.rangeShift = fd.readUShort(offset + Offset.rangeShift.offset); 986 987 int tableOffset = offset + Offset.tableRecordBegin.offset; 988 for (int tableNumber = 0; 989 tableNumber < this.numTables; 990 tableNumber++, tableOffset += Offset.tableRecordSize.offset) { 991 Header table = 992 // safe since the tag is ASCII 993 new Header(fd.readULongAsInt(tableOffset + Offset.tableTag.offset), 994 fd.readULong(tableOffset + Offset.tableCheckSum.offset), // checksum 995 fd.readULongAsInt(tableOffset + Offset.tableOffset.offset), // offset 996 fd.readULongAsInt(tableOffset + Offset.tableLength.offset)); // length 997 records.add(table); 998 } 999 return records; 1000 } 1001 loadTableData( SortedSet<Header> headers, WritableFontData fd)1002 private Map<Header, WritableFontData> loadTableData( 1003 SortedSet<Header> headers, WritableFontData fd) { 1004 Map<Header, WritableFontData> tableData = 1005 new HashMap<Header, WritableFontData>(headers.size()); 1006 logger.fine("######## Reading Table Data"); 1007 for (Header tableHeader : headers) { 1008 WritableFontData data = fd.slice(tableHeader.offset(), tableHeader.length()); 1009 tableData.put(tableHeader, data); 1010 } 1011 return tableData; 1012 } 1013 } 1014 } 1015