public class Font {

  private static final Logger logger =
    Logger.getLogger(Font.class.getCanonicalName());

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  private enum Offset {
    // Offsets within the main directory
    sfntVersion(0),
    numTables(4),
    searchRange(6),
    entrySelector(8),
    rangeShift(10),
    tableRecordBegin(12),
    sfntHeaderSize(12),

    // Offsets within a specific table record
    tableTag(0),
    tableCheckSum(4),
    tableOffset(8),
    tableLength(12),
    tableRecordSize(16);

    private final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  /**
   * Ordering of tables for different font types.
   */
  private final static List<Integer> CFF_TABLE_ORDERING;
  private final static List<Integer> TRUE_TYPE_TABLE_ORDERING;
  static {
    Integer[] cffArray = new Integer[] {Tag.head,
        Tag.hhea,
        Tag.maxp,
        Tag.OS_2,
        Tag.name,
        Tag.cmap,
        Tag.post,
        Tag.CFF};
    List<Integer> cffList = new ArrayList<Integer>(cffArray.length);
    Collections.addAll(cffList, cffArray);
    CFF_TABLE_ORDERING = Collections.unmodifiableList(cffList);

    Integer[] ttArray = new Integer[] {Tag.head,
        Tag.hhea,
        Tag.maxp,
        Tag.OS_2,
        Tag.hmtx,
        Tag.LTSH,
        Tag.VDMX,
        Tag.hdmx,
        Tag.cmap,
        Tag.fpgm,
        Tag.prep,
        Tag.cvt,
        Tag.loca,
        Tag.glyf,
        Tag.kern,
        Tag.name,
        Tag.post,
        Tag.gasp,
        Tag.PCLT,
        Tag.DSIG};
    List<Integer> ttList = new ArrayList<Integer>(ttArray.length);
    Collections.addAll(ttList, ttArray);
    TRUE_TYPE_TABLE_ORDERING = Collections.unmodifiableList(ttList);
  }

  /**
   * Platform ids. These are used in a number of places within the font whenever
   * the platform needs to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum PlatformId {
    Unknown(-1), Unicode(0), Macintosh(1), ISO(2), Windows(3), Custom(4);

    private final int value;

    private PlatformId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static PlatformId valueOf(int value) {
      for (PlatformId platform : PlatformId.values()) {
        if (platform.equals(value)) {
          return platform;
        }
      }
      return Unknown;
    }
  }

  /**
   * Unicode encoding ids. These are used in a number of places within the font
   * whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum UnicodeEncodingId {
    // Unicode Platform Encodings
    Unknown(-1),
    Unicode1_0(0),
    Unicode1_1(1),
    ISO10646(2),
    Unicode2_0_BMP(3),
    Unicode2_0(4),
    UnicodeVariationSequences(5);

    private final int value;

    private UnicodeEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static UnicodeEncodingId valueOf(int value) {
      for (UnicodeEncodingId encoding : UnicodeEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  /**
   * Windows encoding ids. These are used in a number of places within the font
   * whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum WindowsEncodingId {
    // Windows Platform Encodings
    Unknown(-1),
    Symbol(0),
    UnicodeUCS2(1),
    ShiftJIS(2),
    PRC(3),
    Big5(4),
    Wansung(5),
    Johab(6),
    UnicodeUCS4(10);

    private final int value;

    private WindowsEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static WindowsEncodingId valueOf(int value) {
      for (WindowsEncodingId encoding : WindowsEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  /**
   * Macintosh encoding ids. These are used in a number of places within the
   * font whenever character encodings need to be specified.
   *
   * @see NameTable
   * @see CMapTable
   */
  public enum MacintoshEncodingId {
    // Macintosh Platform Encodings
    Unknown(-1),
    Roman(0),
    Japanese(1),
    ChineseTraditional(2),
    Korean(3),
    Arabic(4),
    Hebrew(5),
    Greek(6),
    Russian(7),
    RSymbol(8),
    Devanagari(9),
    Gurmukhi(10),
    Gujarati(11),
    Oriya(12),
    Bengali(13),
    Tamil(14),
    Telugu(15),
    Kannada(16),
    Malayalam(17),
    Sinhalese(18),
    Burmese(19),
    Khmer(20),
    Thai(21),
    Laotian(22),
    Georgian(23),
    Armenian(24),
    ChineseSimplified(25),
    Tibetan(26),
    Mongolian(27),
    Geez(28),
    Slavic(29),
    Vietnamese(30),
    Sindhi(31),
    Uninterpreted(32);

    private final int value;

    private MacintoshEncodingId(int value) {
      this.value = value;
    }

    public int value() {
      return this.value;
    }

    public boolean equals(int value) {
      return value == this.value;
    }

    public static MacintoshEncodingId valueOf(int value) {
      for (MacintoshEncodingId encoding : MacintoshEncodingId.values()) {
        if (encoding.equals(value)) {
          return encoding;
        }
      }
      return Unknown;
    }
  }

  public static final int SFNTVERSION_1 = Fixed1616.fixed(1, 0);

  private final int sfntVersion;
  private final byte[] digest;
  private long checksum;

  private Map<Integer, ? extends Table> tables; // these get set in the builder

  /**
   * Constructor.
   *
   * @param sfntVersion the sfnt version
   * @param digest the computed digest for the font; null if digest was not
   *        computed
   */
  private Font(int sfntVersion, byte[] digest) {
    this.sfntVersion = sfntVersion;
    this.digest = digest;
  }

  /**
   * Gets the sfnt version set in the sfnt wrapper of the font.
   *
   * @return the sfnt version
   */
  public int sfntVersion() {
    return this.sfntVersion;
  }

  /**
   * Gets a copy of the fonts digest that was created when the font was read. If
   * no digest was set at creation time then the return result will be null.
   *
   * @return a copy of the digest array or <code>null</code> if one wasn't set
   *         at creation time
   */
  public byte[] digest() {
    if (this.digest == null) {
      return null;
    }
    return Arrays.copyOf(this.digest, this.digest.length);
  }

  /**
   * Get the checksum for this font.
   *
   * @return the font checksum
   */
  public long checksum() {
    return this.checksum;
  }

  /**
   * Get the number of tables in this font.
   *
   * @return the number of tables
   */
  public int numTables() {
    return this.tables.size();
  }

  /**
   * Get an iterator over all the tables in the font.
   *
   * @return a table iterator
   */
  public Iterator<? extends Table> iterator() {
    return this.tables.values().iterator();
  }

  /**
   * Does the font have a particular table.
   *
   * @param tag the table identifier
   * @return true if the table is in the font; false otherwise
   */
  public boolean hasTable(int tag) {
    return this.tables.containsKey(tag);
  }

  /**
   * Get the table in this font with the specified id.
   *
   * @param <T> the type of the table
   * @param tag the identifier of the table
   * @return the table specified if it exists; null otherwise
   */
  @SuppressWarnings("unchecked")
  public <T extends Table> T getTable(int tag) {
    return (T) this.tables.get(tag);
  }

  /**
   * Get a map of the tables in this font accessed by table tag.
   *
   * @return an unmodifiable view of the tables in this font
   */
  public Map<Integer, ? extends Table> tableMap() {
    return Collections.unmodifiableMap(this.tables);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("digest = ");
    byte[] digest = this.digest();
    if (digest != null) {
      for (int i = 0; i < digest.length; i++) {
        int d = 0xff & digest[i];
        if (d < 0x10) {
          sb.append("0");
        }
        sb.append(Integer.toHexString(d));
      }
    }
    sb.append("\n[");
    sb.append(Fixed1616.toString(sfntVersion));
    sb.append(", ");
    sb.append(this.numTables());
    sb.append("]\n");
    Iterator<? extends Table> iter = this.iterator();
    while (iter.hasNext()) {
      FontDataTable table = iter.next();
      sb.append("\t");
      sb.append(table);
      sb.append("\n");
    }
    return sb.toString();
  }

  /**
   * Serialize the font to the output stream.
   *
   * @param os the destination for the font serialization
   * @param tableOrdering the table ordering to apply
   * @throws IOException
   */
  void serialize(OutputStream os, List<Integer> tableOrdering) throws IOException {
    List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering);
    List<Header> tableRecords = buildTableHeadersForSerialization(finalTableOrdering);
    FontOutputStream fos = new FontOutputStream(os);
    this.serializeHeader(fos, tableRecords);
    this.serializeTables(fos, tableRecords);
  }

  /**
   * Build the table headers to be used for serialization. These headers will be
   * filled out with the data required for serialization. The headers will be
   * sorted in the order specified and only those specified will have headers
   * generated.
   *
   * @param tableOrdering the tables to generate headers for and the order to
   *        sort them
   * @return a list of table headers ready for serialization
   */
  private List<Header> buildTableHeadersForSerialization(List<Integer> tableOrdering) {
    List<Integer> finalTableOrdering = this.generateTableOrdering(tableOrdering);

    List<Header> tableHeaders = new ArrayList<Header>(this.numTables());
    int tableOffset =
        Offset.tableRecordBegin.offset + this.numTables() * Offset.tableRecordSize.offset;
    for (Integer tag : finalTableOrdering) {
      Table table = this.tables.get(tag);
      if (table != null) {
        tableHeaders.add(new Header(
            tag, table.calculatedChecksum(), tableOffset, table.header().length()));
        // write on boundary of 4 bytes
        tableOffset += (table.dataLength() + 3) & ~3;
      }
    }
    return tableHeaders;
  }

  /**
   * Searialize the headers.
   *
   * @param fos the destination stream for the headers
   * @param tableHeaders the headers to serialize
   * @throws IOException
   */
  private void serializeHeader(FontOutputStream fos, List<Header> tableHeaders)
      throws IOException {
    fos.writeFixed(this.sfntVersion);
    fos.writeUShort(tableHeaders.size());
    int log2OfMaxPowerOf2 = FontMath.log2(tableHeaders.size());
    int searchRange = 2 << (log2OfMaxPowerOf2 - 1 + 4);
    fos.writeUShort(searchRange);
    fos.writeUShort(log2OfMaxPowerOf2);
    fos.writeUShort((tableHeaders.size() * 16) - searchRange);

    List<Header> sortedHeaders = new ArrayList<Header>(tableHeaders);
    Collections.sort(sortedHeaders, Header.COMPARATOR_BY_TAG);

    for (Header record : sortedHeaders) {
      fos.writeULong(record.tag());
      fos.writeULong(record.checksum());
      fos.writeULong(record.offset());
      fos.writeULong(record.length());
    }
  }

  /**
   * Serialize the tables.
   *
   * @param fos the destination stream for the headers
   * @param tableHeaders the headers for the tables to serialize
   * @throws IOException
   */
  private void serializeTables(FontOutputStream fos, List<Header> tableHeaders)
      throws IOException {

    for (Header record : tableHeaders) {
      Table table = this.getTable(record.tag());
      if (table == null) {
        throw new IOException("Table out of sync with font header.");
      }
      int tableSize = table.serialize(fos);
      int fillerSize = ((tableSize + 3) & ~3) - tableSize;
      for (int i = 0; i < fillerSize; i++) {
        fos.write(0);
      }
    }
  }

  /**
   * Generate the full table ordering to used for serialization. The full
   * ordering uses the partial ordering as a seed and then adds all remaining
   * tables in the font in an undefined order.
   *
   * @param defaultTableOrdering the partial ordering to be used as a seed for
   *        the full ordering
   * @return the full ordering for serialization
   */
  private List<Integer> generateTableOrdering(List<Integer> defaultTableOrdering) {
    List<Integer> tableOrdering = new ArrayList<Integer>(this.tables.size());
    if (defaultTableOrdering == null) {
      defaultTableOrdering = defaultTableOrdering();
    }

    Set<Integer> tablesInFont = new TreeSet<Integer>(this.tables.keySet());

    // add all the default ordering
    for (Integer tag : defaultTableOrdering) {
      if (this.hasTable(tag)) {
        tableOrdering.add(tag);
        tablesInFont.remove(tag);
      }
    }

    // add all the rest
    for (Integer tag : tablesInFont) {
      tableOrdering.add(tag);
    }

    return tableOrdering;
  }

  /**
   * Get the default table ordering based on the type of the font.
   *
   * @return the default table ordering
   */
  private List<Integer> defaultTableOrdering() {
    if (this.hasTable(Tag.CFF)) {
      return Font.CFF_TABLE_ORDERING;
    }
    return Font.TRUE_TYPE_TABLE_ORDERING;
  }

  /**
   * A builder for a font object. The builder allows the for the creation of
   * immutable {@link Font} objects. The builder is a one use non-thread safe
   * object and cnce the {@link Font} object has been created it is no longer
   * usable. To create a further {@link Font} object new builder will be
   * required.
   *
   * @author Stuart Gill
   *
   */
  public static final class Builder {

    private Map<Integer, Table.Builder<? extends Table>> tableBuilders;
    private FontFactory factory;
    private int sfntVersion = SFNTVERSION_1;
    private int numTables;
    @SuppressWarnings("unused")
    private int searchRange;
    @SuppressWarnings("unused")
    private int entrySelector;
    @SuppressWarnings("unused")
    private int rangeShift;
    private Map<Header, WritableFontData> dataBlocks;
    private byte[] digest;

    private Builder(FontFactory factory) {
      this.factory = factory;
      this.tableBuilders = new HashMap<Integer, Table.Builder<? extends Table>>();
    }

    private void loadFont(InputStream is) throws IOException {
      if (is == null) {
        throw new IOException("No input stream for font.");
      }
      FontInputStream fontIS = null;
      try {
        fontIS = new FontInputStream(is);
        SortedSet<Header> records = readHeader(fontIS);
        this.dataBlocks = loadTableData(records, fontIS);
        this.tableBuilders = buildAllTableBuilders(this.dataBlocks);
      } finally {
        fontIS.close();
      }
    }

    private void loadFont(WritableFontData wfd, int offsetToOffsetTable) throws IOException {
      if (wfd == null) {
        throw new IOException("No data for font.");
      }
      SortedSet<Header> records = readHeader(wfd, offsetToOffsetTable);
      this.dataBlocks = loadTableData(records, wfd);
      this.tableBuilders = buildAllTableBuilders(this.dataBlocks);
    }

    static final Builder
    getOTFBuilder(FontFactory factory, InputStream is) throws IOException {
      Builder builder = new Builder(factory);
      builder.loadFont(is);
      return builder;
    }

    static final Builder getOTFBuilder(
        FontFactory factory, WritableFontData wfd, int offsetToOffsetTable) throws IOException {
      Builder builder = new Builder(factory);
      builder.loadFont(wfd, offsetToOffsetTable);
      return builder;
    }

    static final Builder getOTFBuilder(FontFactory factory) {
      return new Builder(factory);
    }

    /**
     * Get the font factory that created this font builder.
     *
     * @return the font factory
     */
    public FontFactory getFontFactory() {
      return this.factory;
    }

    /**
     * Is the font ready to build?
     *
     * @return true if ready to build; false otherwise
     */
    public boolean readyToBuild() {
      // just read in data with no manipulation
      if (this.tableBuilders == null && this.dataBlocks != null && this.dataBlocks.size() > 0) {
        return true;
      }

      for (Table.Builder<? extends Table> tableBuilder : this.tableBuilders.values()) {
        if (tableBuilder.readyToBuild() == false) {
          return false;
        }
      }
      return true;
    }

    /**
     * Build the {@link Font}. After this call this builder will no longer be
     * usable.
     *
     * @return a {@link Font}
     */
    public Font build() {
      Map<Integer, ? extends Table> tables = null;

      Font font = new Font(this.sfntVersion, this.digest);

      if (this.tableBuilders.size() > 0) {
        tables = buildTablesFromBuilders(font, this.tableBuilders);
      }
      font.tables = tables;
      this.tableBuilders = null;
      this.dataBlocks = null;
      return font;
    }

    /**
     * Set a unique fingerprint for the font object.
     *
     * @param digest a unique identifier for the font
     */
    public void setDigest(byte[] digest) {
      this.digest = digest;
    }

    /**
     * Clear all table builders.
     */
    public void clearTableBuilders() {
      this.tableBuilders.clear();
    }

    /**
     * Does this font builder have the specified table builder.
     *
     * @param tag the table builder tag
     * @return true if there is a builder for that table; false otherwise
     */
    public boolean hasTableBuilder(int tag) {
      return this.tableBuilders.containsKey(tag);
    }

    /**
     * Get the table builder for the given tag. If there is no builder for that
     * tag then return a null.
     *
     * @param tag the table builder tag
     * @return the builder for the tag; null if there is no builder for that tag
     */
    public Table.Builder<? extends Table> getTableBuilder(int tag) {
      Table.Builder<? extends Table> builder = this.tableBuilders.get(tag);
      return builder;
    }

    /**
     * Creates a new empty table builder for the table type given by the table
     * id tag.
     *
     *  This new table will be added to the font and will replace any existing
     * builder for that table.
     *
     * @param tag
     * @return new empty table of the type specified by tag; if tag is not known
     *         then a generic OpenTypeTable is returned
     */
    public Table.Builder<? extends Table> newTableBuilder(int tag) {
      Header header = new Header(tag);
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, null);
      this.tableBuilders.put(header.tag(), builder);

      return builder;
    }

    /**
     * Creates a new table builder for the table type given by the table id tag.
     * It makes a copy of the data provided and uses that copy for the table.
     *
     *  This new table has been added to the font and will replace any existing
     * builder for that table.
     *
     * @param tag
     * @param srcData
     * @return new empty table of the type specified by tag; if tag is not known
     *         then a generic OpenTypeTable is returned
     */
    public Table.Builder<? extends Table> newTableBuilder(int tag, ReadableFontData srcData) {
      WritableFontData data;
      data = WritableFontData.createWritableFontData(srcData.length());
      // TODO(stuartg): take over original data instead?
      srcData.copyTo(data);

      Header header = new Header(tag, data.length());
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data);

      this.tableBuilders.put(tag, builder);

      return builder;
    }

    /**
     * Get a map of the table builders in this font builder accessed by table
     * tag.
     *
     * @return an unmodifiable view of the table builders in this font builder
     */
    public Map<Integer, Table.Builder<? extends Table>> tableBuilderMap() {
      return Collections.unmodifiableMap(this.tableBuilders);
    }

    /**
     * Remove the specified table builder from the font builder.
     *
     * @param tag the table builder to remove
     * @return the table builder removed
     */
    public Table.Builder<? extends Table> removeTableBuilder(int tag) {
      return this.tableBuilders.remove(tag);
    }

    /**
     * Get the number of table builders in the font builder.
     *
     * @return the number of table builders
     */
    public int tableBuilderCount() {
      return this.tableBuilders.size();
    }

    @SuppressWarnings("unused")
    private int sfntWrapperSize() {
      return Offset.sfntHeaderSize.offset +
      (Offset.tableRecordSize.offset * this.tableBuilders.size());
    }

    private Map<Integer, Table.Builder<? extends Table>> buildAllTableBuilders(
        Map<Header, WritableFontData> tableData) {
      Map<Integer, Table.Builder<? extends Table>> builderMap = 
        new HashMap<Integer, Table.Builder<? extends Table>>();
      Set<Header> records = tableData.keySet();
      for (Header record : records) {
        Table.Builder<? extends Table> builder = getTableBuilder(record, tableData.get(record));
        builderMap.put(record.tag(), builder);
      }
      interRelateBuilders(builderMap);
      return builderMap;
    }

    private Table.Builder<? extends Table> getTableBuilder(Header header, WritableFontData data) {
      Table.Builder<? extends Table> builder = Table.Builder.getBuilder(header, data);
      return builder;
    }

    private static Map<Integer, Table> buildTablesFromBuilders(Font font,
        Map<Integer, Table.Builder<? extends Table>> builderMap) {
      Map<Integer, Table> tableMap = new TreeMap<Integer, Table>();

      interRelateBuilders(builderMap);

      long fontChecksum = 0;
      boolean tablesChanged = false;
      FontHeaderTable.Builder headerTableBuilder = null;
      
      // now build all the tables
      for (Table.Builder<? extends Table> builder : builderMap.values()) {
        Table table = null;
        if (Tag.isHeaderTable(builder.header().tag())) {
          headerTableBuilder = (FontHeaderTable.Builder) builder;
          continue;
        }
        if (builder.readyToBuild()) {
          tablesChanged |= builder.changed();
          table = builder.build();
        }
        if (table == null) {
          throw new RuntimeException("Unable to build table - " + builder);
        }
        long tableChecksum = table.calculatedChecksum();
        fontChecksum += tableChecksum;
        tableMap.put(table.header().tag(), table);
      }
      
      // now fix up the header table
      Table headerTable = null;
      if (headerTableBuilder != null) {
        if (tablesChanged) {
          headerTableBuilder.setFontChecksum(fontChecksum);
        }
        if (headerTableBuilder.readyToBuild()) {
          tablesChanged |= headerTableBuilder.changed();
          headerTable = headerTableBuilder.build();
        }
        if (headerTable == null) {
          throw new RuntimeException("Unable to build table - " + headerTableBuilder);
        }
        fontChecksum += headerTable.calculatedChecksum();
        tableMap.put(headerTable.header().tag(), headerTable);
      }
      
      font.checksum = fontChecksum & 0xffffffffL;
      return tableMap;
    }

    private static void
    interRelateBuilders(Map<Integer, Table.Builder<? extends Table>> builderMap) {
      FontHeaderTable.Builder headerTableBuilder =
        (FontHeaderTable.Builder) builderMap.get(Tag.head);
      HorizontalHeaderTable.Builder horizontalHeaderBuilder =
        (HorizontalHeaderTable.Builder) builderMap.get(Tag.hhea);
      MaximumProfileTable.Builder maxProfileBuilder =
        (MaximumProfileTable.Builder) builderMap.get(Tag.maxp);
      LocaTable.Builder locaTableBuilder =
        (LocaTable.Builder) builderMap.get(Tag.loca);
      HorizontalMetricsTable.Builder horizontalMetricsBuilder =
        (HorizontalMetricsTable.Builder) builderMap.get(Tag.hmtx);
      HorizontalDeviceMetricsTable.Builder hdmxTableBuilder =
        (HorizontalDeviceMetricsTable.Builder) builderMap.get(Tag.hdmx);

      // set the inter table data required to build certain tables
      if (horizontalMetricsBuilder != null) {
        if (maxProfileBuilder != null) {
          horizontalMetricsBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
        if (horizontalHeaderBuilder != null) {
          horizontalMetricsBuilder.setNumberOfHMetrics(
              horizontalHeaderBuilder.numberOfHMetrics());
        }
      }

      if (locaTableBuilder != null) {
        if (maxProfileBuilder != null) {
          locaTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
        if (headerTableBuilder != null) {
          locaTableBuilder.setFormatVersion(headerTableBuilder.indexToLocFormat());
        }
      }

      if (hdmxTableBuilder != null) {
        if (maxProfileBuilder != null) {
          hdmxTableBuilder.setNumGlyphs(maxProfileBuilder.numGlyphs());
        }
      }      
    }

    private SortedSet<Header> readHeader(FontInputStream is) throws IOException {
      SortedSet<Header> records =
          new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET);

      this.sfntVersion = is.readFixed();
      this.numTables = is.readUShort();
      this.searchRange = is.readUShort();
      this.entrySelector = is.readUShort();
      this.rangeShift = is.readUShort();

      for (int tableNumber = 0; tableNumber < this.numTables; tableNumber++) {
        Header table = new Header(is.readULongAsInt(), // safe since the tag is ASCII
            is.readULong(),         // checksum
            is.readULongAsInt(),    // offset
            is.readULongAsInt());   // length
        records.add(table);
      }
      return records;
    }

    private Map<Header, WritableFontData> loadTableData(
        SortedSet<Header> headers, FontInputStream is) throws IOException {
      Map<Header, WritableFontData> tableData =
          new HashMap<Header, WritableFontData>(headers.size());
      logger.fine("########  Reading Table Data");
      for (Header tableHeader : headers) {
        is.skip(tableHeader.offset() - is.position());
        logger.finer("\t" + tableHeader);
        logger.finest("\t\tStream Position = " + Integer.toHexString((int) is.position()));
        // don't close this or the whole stream is gone
        FontInputStream tableIS = new FontInputStream(is, tableHeader.length());
        // TODO(stuartg): start tracking bad tables and other errors
        WritableFontData data = WritableFontData.createWritableFontData(tableHeader.length());
        data.copyFrom(tableIS, tableHeader.length());
        tableData.put(tableHeader, data);
      }
      return tableData;
    }

    private SortedSet<Header> readHeader(ReadableFontData fd, int offset) {
      SortedSet<Header> records =
          new TreeSet<Header>(Header.COMPARATOR_BY_OFFSET);

      this.sfntVersion = fd.readFixed(offset + Offset.sfntVersion.offset);
      this.numTables = fd.readUShort(offset + Offset.numTables.offset);
      this.searchRange = fd.readUShort(offset + Offset.searchRange.offset);
      this.entrySelector = fd.readUShort(offset + Offset.entrySelector.offset);
      this.rangeShift = fd.readUShort(offset + Offset.rangeShift.offset);

      int tableOffset = offset + Offset.tableRecordBegin.offset;
      for (int tableNumber = 0;
      tableNumber < this.numTables;
      tableNumber++, tableOffset += Offset.tableRecordSize.offset) {
        Header table =
        // safe since the tag is ASCII
            new Header(fd.readULongAsInt(tableOffset + Offset.tableTag.offset),
                fd.readULong(tableOffset + Offset.tableCheckSum.offset), // checksum
            fd.readULongAsInt(tableOffset + Offset.tableOffset.offset), // offset
            fd.readULongAsInt(tableOffset + Offset.tableLength.offset)); // length
        records.add(table);
      }
      return records;
    }

    private Map<Header, WritableFontData> loadTableData(
        SortedSet<Header> headers, WritableFontData fd) {
      Map<Header, WritableFontData> tableData =
          new HashMap<Header, WritableFontData>(headers.size());
      logger.fine("########  Reading Table Data");
      for (Header tableHeader : headers) {
        WritableFontData data = fd.slice(tableHeader.offset(), tableHeader.length());
        tableData.put(tableHeader, data);
      }
      return tableData;
    }
  }
} public final class FontFactory {
  private static final int LOOKAHEAD_SIZE = 4;

  // font building settings
  private boolean fingerprint = false;

  // font serialization settings
  List<Integer> tableOrdering;

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  private enum Offset {
    // Offsets within the main directory
    TTCTag(0),
    Version(4),
    numFonts(8),
    OffsetTable(12),

    // TTC Version 2.0 extensions
    // offsets from end of OffsetTable
    ulDsigTag(0),
    ulDsigLength(4),
    ulDsigOffset(8);

    private final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  /**
   * Constructor.
   */
  private FontFactory() {
    // Prevent construction.
  }

  /**
   * Factory method for the construction of a font factory.
   *
   * @return a new font factory
   */
  public static FontFactory getInstance(/*buffer builder factory*/) {
    return new FontFactory();
  }

  // font building settings

  /**
   * Toggle whether fonts that are loaded are fingerprinted with a SHA-1 hash.
   * If a font is fingerprinted then a SHA-1 hash is generated at load time and stored in the
   * font. This is useful for uniquely identifying fonts. By default this is turned on.
   * @param fingerprint whether fingerprinting should be turned on or off
   * @see #fingerprintFont()
   * @see Font#digest()
   */
  public void fingerprintFont(boolean fingerprint) {
    this.fingerprint= fingerprint;
  }

  /**
   * Get the state of the fingerprinting option for fonts that are loaded.
   * @return true if fingerprinting is turned on; false otherwise
   * @see #fingerprintFont(boolean)
   * @see Font#digest()
   */
  public boolean fingerprintFont() {
    return this.fingerprint;
  }

  // input stream font loading

  /**
   * Load the font(s) from the input stream. The current settings on the factory
   * are used during the loading process. One or more fonts are returned if the
   * stream contains valid font data. Some font container formats may have more
   * than one font and in this case multiple font objects will be returned. If
   * the data in the stream cannot be parsed or is invalid an array of size zero
   * will be returned.
   *
   * @param is the input stream font data
   * @return one or more fonts
   * @throws IOException
   */
  public Font[] loadFonts(InputStream is) throws IOException {
    PushbackInputStream pbis =
      new PushbackInputStream(new BufferedInputStream(is), FontFactory.LOOKAHEAD_SIZE);
    if (isCollection(pbis)) {
      return loadCollection(pbis);
    }
    return new Font[] {loadSingleOTF(pbis) };
  }

  /**
   * Load the font(s) from the input stream into font builders. The current
   * settings on the factory are used during the loading process. One or more
   * font builders are returned if the stream contains valid font data. Some
   * font container formats may have more than one font and in this case
   * multiple font builder objects will be returned. If the data in the stream
   * cannot be parsed or is invalid an array of size zero will be returned.
   *
   * @param is the input stream font data
   * @return one or more font builders
   * @throws IOException
   */
  public Builder[] loadFontsForBuilding(InputStream is) throws IOException {
    PushbackInputStream pbis =
      new PushbackInputStream(new BufferedInputStream(is), FontFactory.LOOKAHEAD_SIZE);
    if (isCollection(pbis)) {
      return loadCollectionForBuilding(pbis);
    }
    return new Builder[] {loadSingleOTFForBuilding(pbis) };
  }

  private Font loadSingleOTF(InputStream is) throws IOException {
    return loadSingleOTFForBuilding(is).build();
  }

  private Font[] loadCollection(InputStream is) throws IOException {
    Font.Builder[] builders = loadCollectionForBuilding(is);
    Font[] fonts = new Font[builders.length];
    for (int i = 0; i < fonts.length; i++) {
      fonts[i] = builders[i].build();
    }
    return fonts;
  }

  private Font.Builder loadSingleOTFForBuilding(InputStream is) throws IOException {
    MessageDigest digest = null;
    if (this.fingerprintFont()) {
      try {
        digest = MessageDigest.getInstance("SHA-1");
      } catch (NoSuchAlgorithmException e) {
        throw new IOException("Unable to get requested message digest algorithm.", e);
      }
      DigestInputStream dis = new DigestInputStream(is, digest);
      is = dis;
    }
    Builder builder = Builder.getOTFBuilder(this, is);
    if (this.fingerprintFont()) {
      builder.setDigest(digest.digest());
    }
    return builder;
  }

  private Font.Builder[] loadCollectionForBuilding(InputStream is) throws IOException {
    WritableFontData wfd = WritableFontData.createWritableFontData(is.available());
    wfd.copyFrom(is);
    // TOOD(stuartg): add collection loading from a stream
    return loadCollectionForBuilding(wfd);
  }

  static private boolean isCollection(PushbackInputStream pbis) throws IOException {
    byte[] tag = new byte[4];
    pbis.read(tag);
    pbis.unread(tag);
    return Tag.ttcf == Tag.intValue(tag);
  }

  // ByteArray font loading
  /**
   * Load the font(s) from the byte array. The current settings on the factory
   * are used during the loading process. One or more fonts are returned if the
   * stream contains valid font data. Some font container formats may have more
   * than one font and in this case multiple font objects will be returned. If
   * the data in the stream cannot be parsed or is invalid an array of size zero
   * will be returned.
   *
   * @param b the font data
   * @return one or more fonts
   * @throws IOException
   */
  public Font[] loadFonts(byte[] b) throws IOException {
    // TODO(stuartg): make a ReadableFontData when block loading moved to
    // FontFactory
    WritableFontData rfd = WritableFontData.createWritableFontData(b);
    if (isCollection(rfd)) {
      return loadCollection(rfd);
    }
    return new Font[] {loadSingleOTF(rfd)};
  }

  /**
   * Load the font(s) from the byte array into font builders. The current
   * settings on the factory are used during the loading process. One or more
   * font builders are returned if the stream contains valid font data. Some
   * font container formats may have more than one font and in this case
   * multiple font builder objects will be returned. If the data in the stream
   * cannot be parsed or is invalid an array of size zero will be returned.
   *
   * @param b the byte array font data
   * @return one or more font builders
   * @throws IOException
   */
  public Font.Builder[] loadFontsForBuilding(byte[] b) throws IOException {
    WritableFontData wfd = WritableFontData.createWritableFontData(b);
    if (isCollection(wfd)) {
      return loadCollectionForBuilding(wfd);
    }
    return new Font.Builder[] {loadSingleOTFForBuilding(wfd, 0)};
  }

  private Font loadSingleOTF(WritableFontData wfd) throws IOException {
    return loadSingleOTFForBuilding(wfd, 0).build();
  }

  private Font[] loadCollection(WritableFontData wfd) throws IOException {
    Font.Builder[] builders = loadCollectionForBuilding(wfd);
    Font[] fonts = new Font[builders.length];
    for (int i = 0; i < fonts.length; i++) {
      fonts[i] = builders[i].build();
    }
    return fonts;
  }

  private Font.Builder loadSingleOTFForBuilding(WritableFontData wfd, int offsetToOffsetTable)
      throws IOException {
    MessageDigest digest = null;
    if (this.fingerprintFont()) {
      // TODO(stuartg): digest of ByteArray
    }
    Font.Builder builder = Font.Builder.getOTFBuilder(this, wfd, offsetToOffsetTable);
    return builder;
  }

  private Font.Builder[] loadCollectionForBuilding(WritableFontData wfd) throws IOException {
    int ttcTag = wfd.readULongAsInt(Offset.TTCTag.offset);
    long version = wfd.readFixed(Offset.Version.offset);
    int numFonts = wfd.readULongAsInt(Offset.numFonts.offset);

    Font.Builder[] builders = new Font.Builder[numFonts];
    int offsetTableOffset = Offset.OffsetTable.offset;
    for (int fontNumber = 0; fontNumber < numFonts; fontNumber++,
        offsetTableOffset += FontData.DataSize.ULONG.size()) {
      int offset = wfd.readULongAsInt(offsetTableOffset);
      builders[fontNumber] = this.loadSingleOTFForBuilding(wfd, offset);
    }
    return builders;
  }

  static private boolean isCollection(ReadableFontData rfd) {
    byte[] tag = new byte[4];
    rfd.readBytes(0, tag, 0, tag.length);
    return Tag.ttcf == Tag.intValue(tag);
  }

  // font serialization

  /**
   * Serialize the font to the output stream.
   *
   * @param font the font to serialize
   * @param os the destination stream for the font
   * @throws IOException
   */
  public void serializeFont(Font font, OutputStream os) throws IOException {
    // TODO(stuartg) should have serialization options somewhere
    font.serialize(os, tableOrdering);
  }

  /**
   * Set the table ordering to be used in serializing a font. The table ordering
   * is an ordered list of table ids and tables will be serialized in the order
   * given. Any tables whose id is not listed in the ordering will be placed in
   * an unspecified order following those listed.
   *
   * @param tableOrdering the table ordering
   */
  public void setSerializationTableOrdering(List<Integer> tableOrdering) {
    this.tableOrdering = new ArrayList<Integer>(tableOrdering);
  }

  // new fonts

  /**
   * Get an empty font builder for creating a new font from scratch.
   *
   * @return an empty font builder
   */
  public Builder newFontBuilder() {
    return Font.Builder.getOTFBuilder(this);
  }
} public final class Tag {
  public static final int ttcf = Tag.intValue(new byte[]{'t', 't', 'c', 'f'});

  /***********************************************************************************
   *
   * Table Type Tags
   *
   ***********************************************************************************/

  // required tables
  public static final int cmap = Tag.intValue(new byte[]{'c', 'm', 'a', 'p'});
  public static final int head = Tag.intValue(new byte[]{'h', 'e', 'a', 'd'});
  public static final int hhea = Tag.intValue(new byte[]{'h', 'h', 'e', 'a'});
  public static final int hmtx = Tag.intValue(new byte[]{'h', 'm', 't', 'x'});
  public static final int maxp = Tag.intValue(new byte[]{'m', 'a', 'x', 'p'});
  public static final int name = Tag.intValue(new byte[]{'n', 'a', 'm', 'e'});
  public static final int OS_2 = Tag.intValue(new byte[]{'O', 'S', '/', '2'});
  public static final int post = Tag.intValue(new byte[]{'p', 'o', 's', 't'});

  // truetype outline tables
  public static final int cvt = Tag.intValue(new byte[]{'c', 'v', 't', ' '});
  public static final int fpgm = Tag.intValue(new byte[]{'f', 'p', 'g', 'm'});
  public static final int glyf = Tag.intValue(new byte[]{'g', 'l', 'y', 'f'});
  public static final int loca = Tag.intValue(new byte[]{'l', 'o', 'c', 'a'});
  public static final int prep = Tag.intValue(new byte[]{'p', 'r', 'e', 'p'});

  // postscript outline tables
  public static final int CFF = Tag.intValue(new byte[]{'C', 'F', 'F', ' '});
  public static final int VORG = Tag.intValue(new byte[]{'V', 'O', 'R', 'G'});

  // opentype bitmap glyph outlines
  public static final int EBDT = Tag.intValue(new byte[]{'E', 'B', 'D', 'T'});
  public static final int EBLC = Tag.intValue(new byte[]{'E', 'B', 'L', 'C'});
  public static final int EBSC = Tag.intValue(new byte[]{'E', 'B', 'S', 'C'});

  // advanced typographic features
  public static final int BASE = Tag.intValue(new byte[]{'B', 'A', 'S', 'E'});
  public static final int GDEF = Tag.intValue(new byte[]{'G', 'D', 'E', 'F'});
  public static final int GPOS = Tag.intValue(new byte[]{'G', 'P', 'O', 'S'});
  public static final int GSUB = Tag.intValue(new byte[]{'G', 'S', 'U', 'B'});
  public static final int JSTF = Tag.intValue(new byte[]{'J', 'S', 'T', 'F'});

  // other
  public static final int DSIG = Tag.intValue(new byte[]{'D', 'S', 'I', 'G'});
  public static final int gasp = Tag.intValue(new byte[]{'g', 'a', 's', 'p'});
  public static final int hdmx = Tag.intValue(new byte[]{'h', 'd', 'm', 'x'});
  public static final int kern = Tag.intValue(new byte[]{'k', 'e', 'r', 'n'});
  public static final int LTSH = Tag.intValue(new byte[]{'L', 'T', 'S', 'H'});
  public static final int PCLT = Tag.intValue(new byte[]{'P', 'C', 'L', 'T'});
  public static final int VDMX = Tag.intValue(new byte[]{'V', 'D', 'M', 'X'});
  public static final int vhea = Tag.intValue(new byte[]{'v', 'h', 'e', 'a'});
  public static final int vmtx = Tag.intValue(new byte[]{'v', 'm', 't', 'x'});

  // AAT Tables
  // TODO(stuartg): some tables may be missing from this list
  public static final int bsln = Tag.intValue(new byte[]{'b', 's', 'l', 'n'});
  public static final int feat = Tag.intValue(new byte[]{'f', 'e', 'a', 't'});
  public static final int lcar = Tag.intValue(new byte[]{'l', 'c', 'a', 'r'});
  public static final int morx = Tag.intValue(new byte[]{'m', 'o', 'r', 'x'});
  public static final int opbd = Tag.intValue(new byte[]{'o', 'p', 'b', 'd'});
  public static final int prop = Tag.intValue(new byte[]{'p', 'r', 'o', 'p'});

  // Graphite tables
  public static final int Feat = Tag.intValue(new byte[]{'F', 'e', 'a', 't'});
  public static final int Glat = Tag.intValue(new byte[]{'G', 'l', 'a', 't'});
  public static final int Gloc = Tag.intValue(new byte[]{'G', 'l', 'o', 'c'});
  public static final int Sile = Tag.intValue(new byte[]{'S', 'i', 'l', 'e'});
  public static final int Silf = Tag.intValue(new byte[]{'S', 'i', 'l', 'f'});

  // truetype bitmap font tables
  public static final int bhed = Tag.intValue(new byte[]{'b', 'h', 'e', 'd'});
  public static final int bdat = Tag.intValue(new byte[]{'b', 'd', 'a', 't'});
  public static final int bloc = Tag.intValue(new byte[]{'b', 'l', 'o', 'c'});

  private Tag() {
    // Prevent construction.
  }

  public static int intValue(byte[] tag) {
    return tag[0] << 24 | tag[1] << 16 | tag[2] << 8 | tag[3];
  }

  public static byte[] byteValue(int tag) {
    byte[] b = new byte[4];
    b[0] = (byte) (0xff & (tag >> 24));
    b[1] = (byte) (0xff & (tag >> 16));
    b[2] = (byte) (0xff & (tag >> 8));
    b[3] = (byte) (0xff & tag);
    return b;
  }

  public static String stringValue(int tag) {
    String s;
    try {
      s = new String(Tag.byteValue(tag), "US-ASCII");
    } catch (UnsupportedEncodingException e) {
      // should never happen since US-ASCII is a guaranteed character set but...
      return "";
    }
    return s;
  }

  public static int intValue(String s) {
    byte[] b = null;
    try {
      b = s.substring(0, 4).getBytes("US-ASCII");
    } catch (UnsupportedEncodingException e) {
      // should never happen since US-ASCII is a guaranteed character set but...
      return 0;
    }
    return intValue(b);
  }

  /**
   * Determines whether the tag is that for the header table.
   * @param tag table tag
   * @return true if the tag represents the font header table
   */
  public static boolean isHeaderTable(int tag) {
    if (tag == Tag.head || tag == Tag.bhed) {
      return true;
    }
    return false;
  }
} public final class WritableFontData extends ReadableFontData {

  /**
   * Constructs a writable font data object. If the length is specified as
   * positive then a fixed size font data object will be created. If the length
   * is zero or less then a growable font data object will be created and the
   * size will be used as an estimate to help in allocating the original space.
   *
   * @param length if length > 0 create a fixed length font data; otherwise
   *        create a growable font data
   * @return a new writable font data
   */
  public static final WritableFontData createWritableFontData(int length) {
    ByteArray<?> ba = null;
    if (length > 0) {
      ba = new MemoryByteArray(length);
      ba.setFilledLength(length);
    } else {
      ba = new GrowableMemoryByteArray();
    }
    WritableFontData wfd = new WritableFontData(ba);
    return wfd;
  }

  /**
   * Constructs a writable font data object. The new font data object will wrap
   * the bytes passed in to the factory and it will take ownership of those
   * bytes. They should not be used again by the caller.
   *
   * @param b the byte array to wrap
   * @return a new writable font data
   */
  public static final WritableFontData createWritableFontData(byte[] b) {
    ByteArray<?> ba = new MemoryByteArray(b);
    WritableFontData wfd = new WritableFontData(ba);
    return wfd;
  }
  
  /**
   * Constructs a writable font data object. The new font data object will wrap
   * a copy of the the data used by the original writable font data object passed in.
   *
   * @param original the source font data
   * @return a new writable font data
   */
  public static final WritableFontData createWritableFontData(ReadableFontData original) {
    ByteArray<?> ba = null;
    // TODO(stuartg): push this down into the BAs - maybe remove the difference between growable and fixed
    if (original.array.growable()) {
      ba = new GrowableMemoryByteArray();
    } else {
      ba = new MemoryByteArray(original.array.length());
    }
    original.array.copyTo(ba);
    
    WritableFontData wfd = new WritableFontData(ba);
    wfd.setCheckSumRanges(original.checkSumRange());
    return wfd;
  }
  
  /**
   * Constructor.
   *
   * @param array byte array to wrap
   */
  private WritableFontData(ByteArray<? extends ByteArray<?>> array) {
    super(array);
  }

  /**
   * Constructor with a lower bound.
   *
   * @param data other WritableFontData object to share data with
   * @param offset offset from the other WritableFontData's data
   */
  private WritableFontData(WritableFontData data, int offset) {
    super(data, offset);
  }

  /**
   * Constructor with lower bound and a length bound.
   *
   * @param data other WritableFontData object to share data with
   * @param offset offset from the other WritableFontData's data
   * @param length length of other WritableFontData's data to use
   */
  private WritableFontData(WritableFontData data, int offset, int length) {
    super(data, offset, length);
  }

  /**
   * Makes a slice of this FontData. The returned slice will share the data with
   * the original <code>FontData</code>.
   *
   * @param offset the start of the slice
   * @param length the number of bytes in the slice
   * @return a slice of the original FontData
   */
  @Override
  public WritableFontData slice(int offset, int length) {
    if (offset < 0 || (offset + length) > this.size()) {
      throw new IndexOutOfBoundsException("Attempt to bind data outside of its limits.");
    }
    WritableFontData slice = new WritableFontData(this, offset, length);
    return slice;
  }

  /**
   * Makes a bottom bound only slice of this array. The returned slice will
   * share the data with the original <code>FontData</code>.
   *
   * @param offset the start of the slice
   * @return a slice of the original FontData
   */
  @Override
  public WritableFontData slice(int offset) {
    if (offset < 0 || offset > this.size()) {
      throw new IndexOutOfBoundsException("Attempt to bind data outside of its limits.");
    }
    WritableFontData slice = new WritableFontData(this, offset);
    return slice;
  }

  /**
   * Writes a byte at the given index.
   *
   * @param index index into the font data
   * @param b the byte to write
   * @return the number of bytes written
   */
  public int writeByte(int index, byte b) {
    this.array.put(this.boundOffset(index), b);
    return 1;
  }

  /**
   * Writes the bytes from the array.
   *
   * @param index index into the font data
   * @param b the source for the bytes to be written
   * @param offset offset in the byte array
   * @param length the length of the bytes to be written
   * @return the number of bytes actually written; -1 if the index is outside
   *         the FontData's range
   */
  public int writeBytes(int index, byte[] b, int offset, int length) {
    return this.array.put(this.boundOffset(index), b, offset, this.boundLength(index, length));
  }

  /**
   * Writes the bytes from the array and pad if necessary.
   *
   *  Writes to the length given using the byte array provided and if there are
   * not enough bytes in the array then pad to the requested length using the
   * pad byte specified.
   *
   * @param index index into the font data
   * @param b the source for the bytes to be written
   * @param offset offset in the byte array
   * @param length the length of the bytes to be written
   * @param pad the padding byte to be used if necessary
   * @return the number of bytes actually written
   */
  public int writeBytesPad(int index, byte[] b, int offset, int length, byte pad) {
    int written = this.array.put(this.boundOffset(index), b, offset,
        this.boundLength(index, Math.min(length, b.length - offset)));
    written += this.writePadding(written + index, length - written, pad);
    return written;
  }

  /**
   * Writes padding to the FontData. The padding byte written is 0x00.
   *
   * @param index index into the font data
   * @param count the number of pad bytes to write
   * @return the number of pad bytes written
   */
  public int writePadding(int index, int count) {
    return this.writePadding(index, count, (byte) 0x00);
  }

  /**
   * Writes padding to the FontData.
   *
   * @param index index into the font data
   * @param count the number of pad bytes to write
   * @param pad the byte value to use as padding
   * @return the number of pad bytes written
   */
  public int writePadding(int index, int count, byte pad) {
    for (int i = 0; i < count; i++) {
      this.array.put(index + i, pad);
    }
    return count;
  }

  /**
   * Writes the bytes from the array.
   *
   * @param index index into the font data
   * @param b the source for the bytes to be written
   * @return the number of bytes actually written; -1 if the index is outside
   *         the FontData's range
   */
  public int writeBytes(int index, byte[] b) {
    return this.writeBytes(index, b, 0, b.length);
  }

  /**
   * Writes the CHAR at the given index.
   *
   * @param index index into the font data
   * @param c the CHAR
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeChar(int index, byte c) {
    return this.writeByte(index, c);
  }

  /**
   * Writes the USHORT at the given index.
   *
   * @param index index into the font data
   * @param us the USHORT
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeUShort(int index, int us) {
    this.writeByte(index, (byte) ((us >> 8) & 0xff));
    this.writeByte(index + 1, (byte) (us & 0xff));
    return 2;
  }

  /**
   * Writes the USHORT at the given index in little endian format.
   *
   * @param index index into the font data
   * @param us the USHORT
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeUShortLE(int index, int us) {
    this.array.put(index, (byte) (us & 0xff));
    this.array.put(index + 1, (byte) ((us >> 8) & 0xff));
    return 2;
  }

  /**
   * Writes the SHORT at the given index.
   *
   * @param index index into the font data
   * @param s the SHORT
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeShort(int index, int s) {
    return this.writeUShort(index, s);
  }

  /**
   * Writes the UINT24 at the given index.
   *
   * @param index index into the font data
   * @param ui the UINT24
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeUInt24(int index, int ui) {
    this.writeByte(index, (byte) ((ui >> 16) & 0xff));
    this.writeByte(index + 1, (byte) ((ui >> 8) & 0xff));
    this.writeByte(index + 2, (byte) (ui & 0xff));
    return 3;
  }

  /**
   * Writes the ULONG at the given index.
   *
   * @param index index into the font data
   * @param ul the ULONG
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeULong(int index, long ul) {
    this.writeByte(index, (byte) ((ul >> 24) & 0xff));
    this.writeByte(index + 1, (byte) ((ul >> 16) & 0xff));
    this.writeByte(index + 2, (byte) ((ul >> 8) & 0xff));
    this.writeByte(index + 3, (byte) (ul & 0xff));
    return 4;
  }

  /**
   * Writes the ULONG at the given index in little endian format.
   *
   * @param index index into the font data
   * @param ul the ULONG
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeULongLE(int index, long ul) {
    this.array.put(index, (byte) (ul & 0xff));
    this.array.put(index + 1, (byte) ((ul >> 8) & 0xff));
    this.array.put(index + 2, (byte) ((ul >> 16) & 0xff));
    this.array.put(index + 3, (byte) ((ul >> 24) & 0xff));
    return 4;
  }

  /**
   * Writes the LONG at the given index.
   *
   * @param index index into the font data
   * @param l the LONG
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeLong(int index, long l) {
    return this.writeULong(index, l);
  }

  /**
   * Writes the Fixed at the given index.
   *
   * @param index index into the font data
   * @param f the Fixed
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeFixed(int index, int f) {
    return this.writeLong(index, f);
  }

  /**
   * Writes the LONGDATETIME at the given index.
   *
   * @param index index into the font data
   * @param date the LONGDATETIME
   * @return the number of bytes actually written
   * @throws IndexOutOfBoundsException if index is outside the FontData's range
   */
  public int writeDateTime(int index, long date) {
    this.writeULong(index, (date >> 32) & 0xffffffff);
    this.writeULong(index + 4, date & 0xffffffff);
    return 8;
  }

  /**
   * Copy from the InputStream into this FontData.
   *
   * @param is the source
   * @param length the number of bytes to copy
   * @throws IOException
   */
  public void copyFrom(InputStream is, int length) throws IOException {
    this.array.copyFrom(is, length);
  }

  /**
   * Copy everything from the InputStream into this FontData.
   *
   * @param is the source
   * @throws IOException
   */
  public void copyFrom(InputStream is) throws IOException {
    this.array.copyFrom(is);
  }
} public final class CMapTable extends SubTableContainerTable implements Iterable<CMap> {

  /**
   * The .notdef glyph.
   */
  public static final int NOTDEF = 0;

  /**
   * Offsets to specific elements in the underlying data. These offsets are relative to the
   * start of the table or the start of sub-blocks within the table.
   */
  enum Offset {
    version(0),
    numTables(2),
    encodingRecordStart(4),

    // offsets relative to the encoding record
    encodingRecordPlatformId(0),
    encodingRecordEncodingId(2),
    encodingRecordOffset(4),
    encodingRecordSize(8),

    format(0),

    // Format 0: Byte encoding table
    format0Format(0),
    format0Length(2),
    format0Language(4),
    format0GlyphIdArray(6),

    // Format 2: High-byte mapping through table
    format2Format(0),
    format2Length(2),
    format2Language(4),
    format2SubHeaderKeys(6),
    format2SubHeaders(518),
    // offset relative to the subHeader structure
    format2SubHeader_firstCode(0),
    format2SubHeader_entryCount(2),
    format2SubHeader_idDelta(4),
    format2SubHeader_idRangeOffset(6),
    format2SubHeader_structLength(8),

    // Format 4: Segment mapping to delta values
    format4Format(0),
    format4Length(2),
    format4Language(4),
    format4SegCountX2(6),
    format4SearchRange(8),
    format4EntrySelector(10),
    format4RangeShift(12),
    format4EndCount(14),
    format4FixedSize(16),

    // format 6: Trimmed table mapping
    format6Format(0),
    format6Length(2),
    format6Language(4),
    format6FirstCode(6),
    format6EntryCount(8),
    format6GlyphIdArray(10),

    // Format 8: mixed 16-bit and 32-bit coverage
    format8Format(0),
    format8Length(4),
    format8Language(8),
    format8Is32(12),
    format8nGroups(8204),
    format8Groups(8208),
    // ofset relative to the group structure
    format8Group_startCharCode(0),
    format8Group_endCharCode(4),
    format8Group_startGlyphId(8),
    format8Group_structLength(12),

    // Format 10: Trimmed array
    format10Format(0),
    format10Length(4),
    format10Language(8),
    format10StartCharCode(12),
    format10NumChars(16),
    format10Glyphs(20),

    // Format 12: Segmented coverage
    format12Format(0),
    format12Length(4),
    format12Language(8),
    format12nGroups(12),
    format12Groups(16),
    format12Groups_structLength(12),
    // offsets within the group structure
    format12_startCharCode(0),
    format12_endCharCode(4),
    format12_startGlyphId(8),

    // Format 13: Last Resort Font
    format13Format(0),
    format13Length(4),
    format13Language(8),
    format13nGroups(12),
    format13Groups(16),
    format13Groups_structLength(12),
    // offsets within the group structure
    format13_startCharCode(0),
    format13_endCharCode(4),
    format13_glyphId(8),

    // TODO: finish support for format 14
    // Format 14: Unicode Variation Sequences
    format14Format(0),
    format14Length(2);

    final int offset;

    private Offset(int offset) {
      this.offset = offset;
    }
  }

  public static final class CMapId implements Comparable<CMapId> {

    public static final CMapId WINDOWS_BMP =
        CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS2.value());
    public static final CMapId WINDOWS_UCS4 =
        CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS4.value());
    public static final CMapId MAC_ROMAN =
        CMapId.getInstance(PlatformId.Macintosh.value(), MacintoshEncodingId.Roman.value());

    public static CMapId getInstance(int platformId, int encodingId) {
      return new CMapId(platformId, encodingId);
    }

    private final int platformId;
    private final int encodingId;

    private CMapId(int platformId, int encodingId) {
      this.platformId = platformId;
      this.encodingId = encodingId;
    }

    public int platformId() {
      return this.platformId;
    }

    public int encodingId() {
      return this.encodingId;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof CMapId)) {
        return false;
      }
      CMapId otherKey = (CMapId) obj;
      if ((otherKey.platformId == this.platformId) && (otherKey.encodingId == this.encodingId)) {
        return true;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return this.platformId << 8 | this.encodingId;
    }

    @Override
    public int compareTo(CMapId o) {
      return this.hashCode() - o.hashCode();
    }

    @Override
    public String toString() {
      StringBuilder b = new StringBuilder();
      b.append("pid = ");
      b.append(this.platformId);
      b.append(", eid = ");
      b.append(this.encodingId);
      return b.toString();
    }
  }
  
  /**
   * Constructor.
   *
   * @param header header for the table
   * @param data data for the table
   */
  private CMapTable(Header header, ReadableFontData data) {
    super(header, data);
  }

  /**
   * Get the table version.
   *
   * @return table version
   */
  public int version() {
    return this.data.readUShort(Offset.version.offset);
  }

  /**
   * Gets the number of cmaps within the CMap table.
   *
   * @return the number of cmaps
   */
  public int numCMaps() {
    return this.data.readUShort(Offset.numTables.offset);
  }

  /**
   * Returns the index of the cmap with the given CMapId in the table or -1 if a cmap with the
   * CMapId does not exist in the table.
   *
   * @param id the id of the cmap to get the index for; this value cannot be null
   * @return the index of the cmap in the table or -1 if the cmap with the CMapId does not exist in
   *         the table
   */
  // TODO Modify the iterator to be index-based and used here
  public int getCmapIndex(CMapId id) {
    for (int index = 0; index < numCMaps(); index++) {
      if (id.equals(cmapId(index))) {
        return index;
      }
    }

    return -1;
  }

  /**
   * Gets the offset in the table data for the encoding record for the cmap with
   * the given index. The offset is from the beginning of the table.
   *
   * @param index the index of the cmap
   * @return offset in the table data
   */
  private static int offsetForEncodingRecord(int index) {
    return Offset.encodingRecordStart.offset + index * Offset.encodingRecordSize.offset;
  }

  /**
   * Gets the cmap id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the cmap id
   */
  public CMapId cmapId(int index) {
    return CMapId.getInstance(platformId(index), encodingId(index));
  }

  /**
   * Gets the platform id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the platform id
   */
  public int platformId(int index) {
    return this.data.readUShort(
        Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets the encoding id for the cmap with the given index.
   *
   * @param index the index of the cmap
   * @return the encoding id
   */
  public int encodingId(int index) {
    return this.data.readUShort(
        Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets the offset in the table data for the cmap table with the given index.
   * The offset is from the beginning of the table.
   *
   * @param index the index of the cmap
   * @return the offset in the table data
   */
  public int offset(int index) {
    return this.data.readULongAsInt(
        Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
  }

  /**
   * Gets an iterator over all of the cmaps within this CMapTable.
   */
  @Override
  public Iterator<CMap> iterator() {
    return new CMapIterator();
  }

  /**
   * Gets an iterator over the cmaps within this CMap table using the provided
   * filter to select the cmaps returned.
   *
   * @param filter the filter
   * @return iterator over cmaps
   */
  public Iterator<CMap> iterator(CMapFilter filter) {
    return new CMapIterator(filter);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(super.toString());
    sb.append(" = { ");
    for (int i = 0; i < this.numCMaps(); i++) {
      CMap cmap;
      try {
        cmap = this.cmap(i);
      } catch (IOException e) {
        continue;
      }
      sb.append("[0x");
      sb.append(Integer.toHexString(this.offset(i)));
      sb.append(" = ");
      sb.append(cmap);
      if (i < this.numCMaps() - 1) {
        sb.append("], ");
      } else {
        sb.append("]");
      }
    }
    sb.append(" }");
    return sb.toString();
  }

  /**
   * A filter on cmaps.
   */
  public interface CMapFilter {
    /**
     * Test on whether the cmap is acceptable or not.
     *
     * @param cmapId the id of the cmap
     * @return true if the cmap is acceptable; false otherwise
     */
    boolean accept(CMapId cmapId);
  }

  private class CMapIterator implements Iterator<CMap> {
    private int tableIndex = 0;
    private CMapFilter filter;

    private CMapIterator() {
      // no filter - iterate over all cmap subtables
    }

    private CMapIterator(CMapFilter filter) {
      this.filter = filter;
    }

    @Override
    public boolean hasNext() {
      if (this.filter == null) {
        if (this.tableIndex < numCMaps()) {
          return true;
        }
        return false;
      }
      for (; this.tableIndex < numCMaps(); this.tableIndex++) {
        if (filter.accept(cmapId(this.tableIndex))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public CMap next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      try {
        return cmap(this.tableIndex++);
      } catch (IOException e) {
        NoSuchElementException newException =
            new NoSuchElementException("Error during the creation of the CMap.");
        newException.initCause(e);
        throw newException;
      }
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException("Cannot remove a CMap table from an existing font.");
    }
  }

  /**
   * Gets the cmap for the given index.
   *
   * @param index the index of the cmap
   * @return the cmap at the index
   * @throws IOException
   */
  public CMap cmap(int index) throws IOException {
    CMap.Builder<? extends CMap> builder =
        CMapTable.Builder.cmapBuilder(this.readFontData(), index);
    return builder.build();
  }

  /**
   * Gets the cmap with the given ids if it exists.
   *
   * @param platformId the platform id
   * @param encodingId the encoding id
   * @return the cmap if it exists; null otherwise
   */
  public CMap cmap(int platformId, int encodingId) {
    return cmap(CMapId.getInstance(platformId, encodingId));
  }

  public CMap cmap(final CMapId cmapId) {
    Iterator<CMap> cmapIter = this.iterator(new CMapFilter() {
      @Override
      public boolean accept(CMapId foundCMapId) {
        if (cmapId.equals(foundCMapId)) {
          return true;
        }
        return false;
      }
    });
    // can only be one cmap for each set of ids
    if (cmapIter.hasNext()) {
      return cmapIter.next();
    }
    return null;
  }

  /**
   * CMap Table Builder.
   *
   */
  public static class Builder extends SubTableContainerTable.Builder<CMapTable> {

    private int version = 0; // TODO(stuartg): make a CMapTable constant
    private Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders;

    /**
     * Creates a new builder using the header information and data provided.
     *
     * @param header the header information
     * @param data the data holding the table
     * @return a new builder
     */
    public static Builder createBuilder(Header header, WritableFontData data) {
      return new Builder(header, data);
    }

    /**
     * Constructor.
     *
     * @param header the table header
     * @param data the writable data for the table
     */
    protected Builder(Header header, WritableFontData data) {
      super(header, data);
    }

    /**
     * Constructor. This constructor will try to maintain the data as readable
     * but if editing operations are attempted then a writable copy will be made
     * the readable data will be discarded.
     *
     * @param header the table header
     * @param data the readable data for the table
     */
    protected Builder(Header header, ReadableFontData data) {
      super(header, data);
    }

    /**
     * Static factory method to create a cmap subtable builder.
     *
     * @param data the data for the whole cmap table
     * @param index the index of the cmap subtable within the table
     * @return the cmap subtable requested if it exists; null otherwise
     */
    protected static CMap.Builder<? extends CMap> cmapBuilder(ReadableFontData data, int index) {
      if (index < 0 || index > numCMaps(data)) {
        throw new IndexOutOfBoundsException(
            "CMap table is outside the bounds of the known tables.");
      }

      // read from encoding records
      int platformId = data.readUShort(
          Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
      int encodingId = data.readUShort(
          Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
      int offset = data.readULongAsInt(
          Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
      CMapId cmapId = CMapId.getInstance(platformId, encodingId);

      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(data, offset, cmapId);
      return builder;
    }

    @Override
    protected void subDataSet() {
      this.cmapBuilders = null;
      super.setModelChanged(false);
    }

    private void initialize(ReadableFontData data) {
      this.cmapBuilders = new /*TreeMap*/ HashMap<CMapId, CMap.Builder<? extends CMap>>();

      int numCMaps = numCMaps(data);
      for (int i = 0; i < numCMaps; i++) {
        CMap.Builder<? extends CMap> cmapBuilder = cmapBuilder(data, i);
        cmapBuilders.put(cmapBuilder.cmapId(), cmapBuilder);
      }
    }

    private Map<CMapId, CMap.Builder<? extends CMap>> getCMapBuilders() {
      if (this.cmapBuilders != null) {
        return this.cmapBuilders;
      }
      this.initialize(this.internalReadData());
      this.setModelChanged();

      return this.cmapBuilders;
    }

    private static int numCMaps(ReadableFontData data) {
      if (data == null) {
        return 0;
      }
      return data.readUShort(Offset.numTables.offset);
    }

    public int numCMaps() {
      return this.getCMapBuilders().size();
    }

    @Override
    protected int subDataSizeToSerialize() {
      if (this.cmapBuilders == null || this.cmapBuilders.size() == 0) {
        return 0;
      }

      boolean variable = false;
      int size = CMapTable.Offset.encodingRecordStart.offset + this.cmapBuilders.size()
      * CMapTable.Offset.encodingRecordSize.offset;

      // calculate size of each table
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        int cmapSize = b.subDataSizeToSerialize();
        size += Math.abs(cmapSize);
        variable |= cmapSize <= 0;
      }
      return variable ? -size : size;
    }

    @Override
    protected boolean subReadyToSerialize() {
      if (this.cmapBuilders == null) {
        return false;
      }
      // check each table
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        if (!b.subReadyToSerialize()) {
          return false;
        }
      }
      return true;
    }

    @Override
    protected int subSerialize(WritableFontData newData) {
      int size = newData.writeUShort(CMapTable.Offset.version.offset, this.version());
      size += newData.writeUShort(CMapTable.Offset.numTables.offset, this.cmapBuilders.size());

      int indexOffset = size;
      size += this.cmapBuilders.size() * CMapTable.Offset.encodingRecordSize.offset;
      for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
        // header entry
        indexOffset += newData.writeUShort(indexOffset, b.platformId());
        indexOffset += newData.writeUShort(indexOffset, b.encodingId());
        indexOffset += newData.writeULong(indexOffset, size);

        // cmap
        size += b.subSerialize(newData.slice(size));
      }
      return size;
    }

    @Override
    protected CMapTable subBuildTable(ReadableFontData data) {
      return new CMapTable(this.header(), data);
    }

    // public building API

    public Iterator<? extends CMap.Builder<? extends CMap>> iterator() {
      return this.getCMapBuilders().values().iterator();
    }

    public int version() {
      return this.version;
    }

    public void setVersion(int version) {
      this.version = version;
    }

    /**
     * Gets a new cmap builder for this cmap table. The new cmap builder will be
     * for the cmap id specified and initialized with the data given. The data
     * will be copied and the original data will not be modified.
     *
     * @param cmapId the id for the new cmap builder
     * @param data the data to copy for the new cmap builder
     * @return a new cmap builder initialized with the cmap id and a copy of the
     *         data
     * @throws IOException
     */
    public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, ReadableFontData data)
        throws IOException {
      WritableFontData wfd = WritableFontData.createWritableFontData(data.size());
      data.copyTo(wfd);
      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(wfd, 0, cmapId);
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      cmapBuilders.put(cmapId, builder);
      return builder;
    }

    public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, CMapFormat cmapFormat) {
      CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(cmapFormat, cmapId);
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      cmapBuilders.put(cmapId, builder);
      return builder;
    }

    public CMap.Builder<? extends CMap> cmapBuilder(CMapId cmapId) {
      Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
      return cmapBuilders.get(cmapId);
    }

  }
} public class EOTWriter {

  private final boolean compressed;
  
  private final FontFactory factory = FontFactory.getInstance();

  private final static long RESERVED = 0;
  private final static short PADDING = 0;
  private final static long VERSION = 0x00020002;
  private final static short MAGIC_NUMBER = 0x504c;
  private final static long DEFAULT_FLAGS = 0;
  private final static long FLAGS_TT_COMPRESSED = 0x4;
  private final static byte DEFAULT_CHARSET = 1;
  private final static long CS_XORKEY = 0x50475342;

  public EOTWriter() {
    compressed = false;
  }
  
  public EOTWriter(boolean compressed) {
    this.compressed = compressed;
  }

  public WritableFontData convert(Font font) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    factory.serializeFont(font, baos);
    byte[] fontData = baos.toByteArray();
    NameTable name = font.getTable(Tag.name);
    byte[] familyName = convertUTF16StringToLittleEndian(name.nameAsBytes(3, 1, 0x409, 1));
    byte[] styleName = convertUTF16StringToLittleEndian(name.nameAsBytes(3, 1, 0x409, 2));
    byte[] versionName = convertUTF16StringToLittleEndian(name.nameAsBytes(3, 1, 0x409, 5));
    byte[] fullName = convertUTF16StringToLittleEndian(name.nameAsBytes(3, 1, 0x409, 4));
    long flags = DEFAULT_FLAGS;
    
    if (compressed) {
      flags |= FLAGS_TT_COMPRESSED;
      MtxWriter mtxWriter = new MtxWriter();
      fontData = mtxWriter.compress(font);
    }
    
    long eotSize = computeEotSize(
      familyName.length, styleName.length, versionName.length, fullName.length, fontData.length);

    WritableFontData writableFontData = createWritableFontData((int) eotSize);

    OS2Table os2Table = font.getTable(Tag.OS_2);
    int index = 0;

    index += writableFontData.writeULongLE(index, eotSize); // EOTSize
    index += writableFontData.writeULongLE(index, fontData.length); // FontDataSize
    index += writableFontData.writeULongLE(index, VERSION); // Version
    index += writableFontData.writeULongLE(index, flags); // Flags
    index += writeFontPANOSE(index, os2Table, writableFontData); // FontPANOSE
    index += writableFontData.writeByte(index, DEFAULT_CHARSET); // Charset
    index += writableFontData.writeByte(index, (byte) (os2Table.fsSelectionAsInt() & 1)); // Italic
    index += writableFontData.writeULongLE(index, os2Table.usWeightClass()); // Weight
    index += writableFontData.writeUShortLE(index, (short) os2Table.fsTypeAsInt()); // fsType
    index += writableFontData.writeUShortLE(index, MAGIC_NUMBER); // MagicNumber
    index += writeUnicodeRanges(index, os2Table, writableFontData); // UnicodeRange{1, 2, 3, 4}
    index += writeCodePages(index, os2Table, writableFontData); // CodePageRange{1, 2}

    FontHeaderTable head = font.getTable(Tag.head);
    index += writableFontData.writeULongLE(index, head.checkSumAdjustment()); // CheckSumAdjustment

    index += writeReservedFields(index, writableFontData); // Reserved{1, 2, 3, 4}
    index += writePadding(index, writableFontData);

    // FamilyNameSize, FamilyName[FamilyNameSize]
    index += writeUTF16String(index, familyName, writableFontData); 
    index += writePadding(index, writableFontData);

    // StyleNameSize, StyleName[StyleNameSize]
    index += writeUTF16String(index, styleName, writableFontData);
    index += writePadding(index, writableFontData);

    // VersionNameSize, VersionName[VersionNameSize]
    index += writeUTF16String(index, versionName, writableFontData);
    index += writePadding(index, writableFontData);

    // FullNameSize, FullName[FullNameSize]
    index += writeUTF16String(index, fullName, writableFontData);
    index += writePadding(index, writableFontData);

    index += writePadding(index, writableFontData); // RootStringSize
    if (VERSION > 0x20001) {
      index += writableFontData.writeULongLE(index, CS_XORKEY);  // RootStringCheckSum
      index += writableFontData.writeULongLE(index, 0);  // EUDCCodePage
      index += writePadding(index, writableFontData);
      index += writePadding(index, writableFontData);  // SignatureSize
      index += writableFontData.writeULongLE(index, 0);  // EUDCFlags
      index += writableFontData.writeULongLE(index, 0);  // EUDCFontSize
    }
    writableFontData.writeBytes(index, fontData, 0, fontData.length); // FontData[FontDataSize]
    return writableFontData;
  }

  private long computeEotSize(int familyNameSize, int styleNameSize, int versionNameSize,
      int fullNameSize, int fontDataSize) {
    return 16 * ReadableFontData.DataSize.ULONG.size() +
        12 * ReadableFontData.DataSize.BYTE.size() +
        12 * ReadableFontData.DataSize.USHORT.size() +
        familyNameSize * ReadableFontData.DataSize.BYTE.size() +
        styleNameSize * ReadableFontData.DataSize.BYTE.size() +
        versionNameSize * ReadableFontData.DataSize.BYTE.size() +
        fullNameSize * ReadableFontData.DataSize.BYTE.size() +
        fontDataSize * ReadableFontData.DataSize.BYTE.size() +
        (VERSION > 0x20001 ? 5 * ReadableFontData.DataSize.ULONG.size() : 0);
  }

  private int writeFontPANOSE(int index, OS2Table os2Table, WritableFontData writableFontData) {
    byte[] fontPANOSE = os2Table.panose();
    return writableFontData.writeBytes(index, fontPANOSE, 0, fontPANOSE.length);
  }

  private int writeReservedFields(int start, WritableFontData writableFontData) {
    int index = start;
    for (int i = 0; i < 4; i++) {
      index += writableFontData.writeULongLE(index, RESERVED);
    }
    return index - start;
  }

  private int writeUnicodeRanges(int start, OS2Table os2Table, WritableFontData writableFontData) {
    int index = start;

    // TODO: change to loop when os2Table.ulUnicodeRange() is implemented
    index += writableFontData.writeULongLE(index, os2Table.ulUnicodeRange1());
    index += writableFontData.writeULongLE(index, os2Table.ulUnicodeRange2());
    index += writableFontData.writeULongLE(index, os2Table.ulUnicodeRange3());
    index += writableFontData.writeULongLE(index, os2Table.ulUnicodeRange4());
    return index - start;
  }

  private int writeCodePages(int start, OS2Table os2Table, WritableFontData writableFontData) {
    int index = start;
    index += writableFontData.writeULongLE(index, os2Table.ulCodePageRange1());
    index += writableFontData.writeULongLE(index, os2Table.ulCodePageRange2());
    return index - start;
  }

  private int writePadding(int index, WritableFontData writableFontData) {
    return writableFontData.writeUShortLE(index, PADDING);
  }

  private int writeUTF16String(int start, byte[] str, WritableFontData writableFontData) {
    int index = start;
    index += writableFontData.writeUShortLE(index, (short) str.length);
    index += writableFontData.writeBytes(index, str, 0, str.length);
    return index - start;
  }

  private byte[] convertUTF16StringToLittleEndian(byte[] bytesString) {
    if (bytesString == null) {
      return new byte[0];
    }
    for (int i = 0; i < bytesString.length; i+= 2) {
      byte tmp = bytesString[i];
      bytesString[i] = bytesString[i + 1];
      bytesString[i + 1] = tmp;
    }
    return bytesString;
  }

  private WritableFontData createWritableFontData(int length) {
    return WritableFontData.createWritableFontData(length);
  }
} public class WoffWriter {

  protected boolean woff_compression_faster = false;

  private static final long SIGNATURE = 0x774F4646;
  private static final int WOFF_HEADER_SIZE =
      (9 * FontData.DataSize.ULONG.size()) + (4 * FontData.DataSize.USHORT.size());

  public WritableFontData convert(Font font) {
    List<TableDirectoryEntry> tableDirectoryEntries = createTableDirectoryEntries(font);
    int length =
        WOFF_HEADER_SIZE + computeTableDirectoryEntriesLength(tableDirectoryEntries)
            + computeTablesLength(tableDirectoryEntries);
    WritableFontData writableFontData = WritableFontData.createWritableFontData(length);
    int index = 0;

    index += writeWoffHeader(writableFontData,
        index,
        tableDirectoryEntries,
        font.sfntVersion(),
        length,
        extractMajorVersion(font),
        extractMinorVersion(font));
    index += writeTableDirectoryEntries(writableFontData, index, tableDirectoryEntries);
    index += writeTables(writableFontData, index, tableDirectoryEntries);
    return writableFontData;
  }

  private int extractMajorVersion(Font font) {
    FontHeaderTable head = (FontHeaderTable) font.getTable(Tag.head);
    return (head.fontRevision() >> 16) & 0xffff;
  }

  private int extractMinorVersion(Font sfntlyFont) {
    FontHeaderTable head = (FontHeaderTable) sfntlyFont.getTable(Tag.head);
    return head.fontRevision() & 0xffff;
  }

  private int align4(int value) {
    return (value + 3) & -4;
  }

  private int computeTableDirectoryEntriesLength(
      List<TableDirectoryEntry> tableDirectoryEntries) {
    return TableDirectoryEntry.ENTRY_SIZE * tableDirectoryEntries.size();
  }

  private int computeTablesLength(List<TableDirectoryEntry> tableDirectoryEntries) {
    int length = 0;
    for (TableDirectoryEntry entry : tableDirectoryEntries) {
      length += entry.getCompressedTableLength();
      length = align4(length);
    }
    return length;
  }

  private int writeWoffHeader(WritableFontData writableFontData,
      int start,
      List<TableDirectoryEntry> tableDirectoryEntries,
      int flavor,
      int length,
      int majorVersion,
      int minorVersion) {
    int index = start;
    index += writableFontData.writeULong(index, SIGNATURE); // signature
    index += writableFontData.writeULong(index, flavor); // flavor
    index += writableFontData.writeULong(index, length); // length
    index += writableFontData.writeUShort(index, tableDirectoryEntries.size()); // numTables
    index += writableFontData.writeUShort(index, 0); // reserved

    // totalSfntSize
    index +=
        writableFontData.writeULong(index, computeUncompressedTablesLength(tableDirectoryEntries)
            + computeTableSfntHeaderLength(tableDirectoryEntries));

    index += writableFontData.writeUShort(index, 1); // majorVersion
    index += writableFontData.writeUShort(index, 1); // minorVersion
    index += writableFontData.writeULong(index, 0); // metaOffset
    index += writableFontData.writeULong(index, 0); // metaLength
    index += writableFontData.writeULong(index, 0); // metaOrigLength
    index += writableFontData.writeULong(index, 0); // privOffset
    index += writableFontData.writeULong(index, 0); // privLength
    return WOFF_HEADER_SIZE;
  }

  private int computeTableSfntHeaderLength(List<TableDirectoryEntry> tableDirectoryEntries) {
    return FontData.DataSize.ULONG.size() + (4 * FontData.DataSize.USHORT.size())
        + ((4 * FontData.DataSize.ULONG.size()) * tableDirectoryEntries.size());
  }

  private int computeUncompressedTablesLength(List<TableDirectoryEntry> tableDirectoryEntries) {
    int length = 0;
    for (TableDirectoryEntry entry : tableDirectoryEntries) {
      length += entry.getUncompressedTableLength();
      length = align4(length);
    }
    return length;
  }

  private int writeTableDirectoryEntries(WritableFontData writableFontData, int start,
      List<TableDirectoryEntry> tableDirectoryEntries) {
    int index = start;
    int tableOffset = align4(start + computeTableDirectoryEntriesLength(tableDirectoryEntries));
    for (TableDirectoryEntry entry : tableDirectoryEntries) {
      index += entry.writeEntry(writableFontData, tableOffset, index);
      tableOffset += entry.getCompressedTableLength();
      tableOffset = align4(tableOffset);
    }
    return computeTableDirectoryEntriesLength(tableDirectoryEntries);
  }

  private int writeTables(WritableFontData writableFontData, int start,
      List<TableDirectoryEntry> tableDirectoryEntries) {
    int index = align4(start);
    for (TableDirectoryEntry entry : tableDirectoryEntries) {
      index += entry.writeTable(writableFontData, index);
      index = align4(index);
    }
    return index - start;
  }

  private List<TableDirectoryEntry> createTableDirectoryEntries(Font font) {
    List<TableDirectoryEntry> tableDirectoryEntries = new ArrayList<TableDirectoryEntry>();
    TreeSet<Integer> tags = new TreeSet<Integer>(font.tableMap().keySet());
    tags.remove(Tag.DSIG);

    for (int tag : tags) {
      Table table = font.getTable(tag);
      TableDirectoryEntry tableDirectoryEntry = new TableDirectoryEntry();
      tableDirectoryEntry.setTag(tag);
      tableDirectoryEntry.setOrigLength(table.dataLength());
      tableDirectoryEntry.setOrigChecksum(table.calculatedChecksum());
      setCompressedTableData(tableDirectoryEntry, table);
      tableDirectoryEntries.add(tableDirectoryEntry);
    }
    return tableDirectoryEntries;
  }

  private void setCompressedTableData(TableDirectoryEntry tableDirectoryEntry, Table table) {
    int length = table.dataLength();
    byte[] input = new byte[length];
    table.readFontData().readBytes(0, input, 0, length);
    if (woff_compression_faster && (length < 100 || table.headerTag() == Tag.loca)) {
      tableDirectoryEntry.setCompTable(input);
    } else {
      byte[] output = new byte[length];
      Deflater compresser = new Deflater();
      compresser.setInput(input);
      compresser.finish();
      int compLength = compresser.deflate(output);
      tableDirectoryEntry.setCompTable(
          compLength == length || !compresser.finished() ? input : Arrays.copyOfRange(
              output, 0, compLength));
    }
  }

  private static class TableDirectoryEntry {

    public static final int ENTRY_SIZE = 5 * FontData.DataSize.ULONG.size();

    private long tag;
    private long origLength;
    private long origChecksum;
    private byte[] compTable;

    public void setTag(int tag) {
      this.tag = tag;
    }

    public void setOrigLength(int origLength) {
      this.origLength = origLength;
    }

    public void setOrigChecksum(long origChecksum) {
      this.origChecksum = origChecksum;
    }

    public void setCompTable(byte[] compTable) {
      this.compTable = compTable;
    }

    public int getCompressedTableLength() {
      return compTable.length;
    }

    public long getUncompressedTableLength() {
      return origLength;
    }

    public int writeEntry(WritableFontData writableFontData, int tableOffset, int start) {
      int index = start;
      index += writableFontData.writeULong(index, tag);
      index += writableFontData.writeULong(index, tableOffset);
      index += writableFontData.writeULong(index, compTable.length);
      index += writableFontData.writeULong(index, origLength);
      index += writableFontData.writeULong(index, origChecksum);
      return ENTRY_SIZE;
    }

    public int writeTable(WritableFontData writableFontData, int index) {
      writableFontData.writeBytes(index, compTable, 0, compTable.length);
      return getCompressedTableLength();
    }
  }
} public class HintStripper extends Subsetter {

  {
    Set<TableSubsetter> temp = new HashSet<TableSubsetter>();
    temp.add(new GlyphTableStripper());
    tableSubsetters = temp;
  }

  public HintStripper(Font font, FontFactory fontFactory) {
    super(font, fontFactory);
  }
} public class RenumberingSubsetter extends Subsetter {

  {
    Set<TableSubsetter> temp = new HashSet<TableSubsetter>();
    temp.add(new GlyphTableSubsetter());
    temp.add(new RenumberingCMapTableSubsetter());
    temp.add(new PostScriptTableSubsetter());
    temp.add(new HorizontalMetricsTableSubsetter());
    tableSubsetters = temp;
  }

  public RenumberingSubsetter(Font font, FontFactory fontFactory) {
    super(font, fontFactory);
  }

  @Override
  protected void setUpTables(Font.Builder fontBuilder) {
    fontBuilder.newTableBuilder(Tag.hhea, font.getTable(Tag.hhea).readFontData());
    fontBuilder.newTableBuilder(Tag.maxp, font.getTable(Tag.maxp).readFontData());
  }
} public class Subsetter {

  protected final Font font;

  // TODO(stuartg): add SmartFontBuilder
  //private SmartFontBuilder fontBuilder;
  private FontFactory fontFactory;
  // TODO(stuartg): can TableSubsetter become TableProcessor?
  protected Set<TableSubsetter> tableSubsetters;

  // settings from user
  private Set<Integer> removeTables;
  private List<Integer> newToOldGlyphs;
  private List<CMapTable.CMapId> cmapIds;

  // inverse of mapping, computed lazily
  private Map<Integer, Integer> oldToNewGlyphs = null;

  protected Subsetter(Font font, FontFactory fontFactory) {
    this.font = font;
    this.fontFactory = fontFactory;
  }

  public void setGlyphs(List<Integer> glyphs) {
    this.newToOldGlyphs = new ArrayList<Integer>(glyphs);
  }

  /**
   * Set the cmaps to be used in the subsetted font. The cmaps are listed in
   * order of priority and the number parameter gives a count of how many of the
   * list should be put into the subsetted font. If there are no matches in the
   * font for any of the provided cmap ids which would lead to a font with no
   * cmap then an error will be thrown during subsetting.
   *
   * The two most common cases would be: <list>
   * <ul>
   * a list of one or more cmap ids with a count setting of 1 <br>This will use
   * the list of cmap ids as an ordered priority and look for an available cmap
   * in the font that matches the requests. Only the first such match will be
   * placed in the subsetted font.
   * </ul>
   * <ul>
   * a list of one or more cmap ids with a count setting equal to the list
   * length <br>This will use the list of cmap ids and try to place each one
   * specified into the subsetted font.
   * </ul>
   * </list>
   *
   * @param cmapIds the cmap ids to use for the subsetted font
   * @param number the maximum number of cmaps to place in the subsetted font
   */
  public void setCMaps(List<CMapTable.CMapId> cmapIds, int number) {
    this.cmapIds = new ArrayList<CMapTable.CMapId>();
    CMapTable cmapTable = this.font.getTable(Tag.cmap);
    if (cmapTable == null) {
      throw new InvalidParameterException("Font has no cmap table.");
    }
    for (CMapTable.CMapId cmapId : cmapIds) {
      CMap cmap = cmapTable.cmap(cmapId);
      if (cmap != null) {
        this.cmapIds.add(cmap.cmapId());
        number--;
        if (number <= 0) {
          break;
        }
      }
    }
    if (this.cmapIds.size() == 0) {
      this.cmapIds = null;
      throw new InvalidParameterException(
          "CMap Id settings would generate font with no cmap sub-table.");
    }
  }

  public void setRemoveTables(Set<Integer> removeTables) {
    this.removeTables = new HashSet<Integer>(removeTables);
  }

  public Font.Builder subset() throws IOException {
    Font.Builder fontBuilder = this.fontFactory.newFontBuilder();

    setUpTables(fontBuilder);

    Set<Integer> tableTags = new TreeSet<Integer>(this.font.tableMap().keySet());
    if (this.removeTables != null) {
      tableTags.removeAll(this.removeTables);
    }

    for (TableSubsetter tableSubsetter : this.tableSubsetters) {
      boolean handled = tableSubsetter.subset(this, this.font, fontBuilder);
      if (handled) {
        tableTags.removeAll(tableSubsetter.tagsHandled());
      }
    }
    for (Integer tag : tableTags) {
      Table table = this.font.getTable(tag);
      if (table != null) {
        fontBuilder.newTableBuilder(tag, table.readFontData());
      }
    }
    return fontBuilder;
  }

  /**
   * Get the permutation table of the old glyph id to the new glyph id.
   *
   * @return the permutation table
   */
  List<Integer> glyphMappingTable() {
    return this.newToOldGlyphs;
  }

  /**
   * Get the inverse mapping, from new glyph id to old.
   *
   * @return the inverse mapping
   */
  Map<Integer, Integer> getInverseMapping() {
    if (oldToNewGlyphs == null) {
      oldToNewGlyphs = new HashMap<Integer, Integer>();
      List<Integer> mapping = glyphMappingTable();
      for (int i = 0; i < mapping.size(); i++) {
        oldToNewGlyphs.put(mapping.get(i), i);
      }
    }
    return oldToNewGlyphs;
  }

  List<CMapTable.CMapId> cmapId() {
    return this.cmapIds;
  }

  // A hook for subclasses to override, to set up tables.
  protected void setUpTables(Font.Builder fontBuilder) {
  }
}
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.core.CMapTable;
import com.google.typography.font.tools.conversion.eot.EOTWriter;
import com.google.typography.font.tools.conversion.woff.WoffWriter;
import com.google.typography.font.tools.subsetter.HintStripper;
import com.google.typography.font.tools.subsetter.RenumberingSubsetter;
import com.google.typography.font.tools.subsetter.Subsetter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * 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.google.typography.font.tools.sfnttool;



/**
 * @author Raph Levien
 */
public class SfntTool {
  private boolean strip = false;
  private String subsetString = null;
  private boolean woff = false;
  private boolean eot = false;
  private boolean mtx = false;

  public static void main(String[] args) throws IOException {
    SfntTool tool = new SfntTool();
    File fontFile = null;
    File outputFile = null;
    boolean bench = false;
    int nIters = 1;

    for (int i = 0; i < args.length; i++) {
      String option = null;
      if (args[i].charAt(0) == '-') {
        option = args[i].substring(1);
      }

      if (option != null) {
        if (option.equals("help") || option.equals("?")) {
          printUsage();
          System.exit(0);
        } else if (option.equals("b") || option.equals("bench")) {
          nIters = 10000;
        } else if (option.equals("h") || option.equals("hints")) {
          tool.strip = true;
        } else if (option.equals("s") || option.equals("string")) {
          tool.subsetString = args[i + 1];
          i++;
        } else if (option.equals("w") || option.equals("woff")) {
          tool.woff = true;
        } else if (option.equals("e") || option.equals("eot")) {
          tool.eot = true;
        } else if (option.equals("x") || option.equals("mtx")) {
          tool.mtx = true;
        } else {
          printUsage();
          System.exit(1);
        }
      } else {
        if (fontFile == null) {
          fontFile = new File(args[i]);
        } else {
          outputFile = new File(args[i]);
          break;
        }
      }
    }

    if (tool.woff && tool.eot) {
      System.out.println("WOFF and EOT options are mutually exclusive");
      System.exit(1);
    }

    if (fontFile != null && outputFile != null) {
      tool.subsetFontFile(fontFile, outputFile, nIters);
    } else {
      printUsage();
    }
  }

  private static final void printUsage() {
    System.out.println("Subset [-?|-h|-help] [-b] [-s string] fontfile outfile");
    System.out.println("Prototype font subsetter");
    System.out.println("\t-?,-help\tprint this help information");
    System.out.println("\t-s,-string\t String to subset");
    System.out.println("\t-b,-bench\t Benchmark (run 10000 iterations)");
    System.out.println("\t-h,-hints\t Strip hints");
    System.out.println("\t-w,-woff\t Output WOFF format");
    System.out.println("\t-e,-eot\t Output EOT format");
    System.out.println("\t-x,-mtx\t Enable Microtype Express compression for EOT format");
  }

  public void subsetFontFile(File fontFile, File outputFile, int nIters)
      throws IOException {
    FontFactory fontFactory = FontFactory.getInstance();
    FileInputStream fis = null;
    try {
      fis = new FileInputStream(fontFile);
      byte[] fontBytes = new byte[(int)fontFile.length()];
      fis.read(fontBytes);
      Font[] fontArray = null;
      fontArray = fontFactory.loadFonts(fontBytes);
      Font font = fontArray[0];
      List<CMapTable.CMapId> cmapIds = new ArrayList<CMapTable.CMapId>();
      cmapIds.add(CMapTable.CMapId.WINDOWS_BMP);
      byte[] newFontData = null;
      for (int i = 0; i < nIters; i++) {
        Font newFont = font;
        if (subsetString != null) {
          Subsetter subsetter = new RenumberingSubsetter(newFont, fontFactory);
          subsetter.setCMaps(cmapIds, 1);
          List<Integer> glyphs = GlyphCoverage.getGlyphCoverage(font, subsetString);
          subsetter.setGlyphs(glyphs);
          Set<Integer> removeTables = new HashSet<Integer>();
          // Most of the following are valid tables, but we don't renumber them yet, so strip
          removeTables.add(Tag.GDEF);
          removeTables.add(Tag.GPOS);
          removeTables.add(Tag.GSUB);
          removeTables.add(Tag.kern);
          removeTables.add(Tag.hdmx);
          removeTables.add(Tag.vmtx);
          removeTables.add(Tag.VDMX);
          removeTables.add(Tag.LTSH);
          removeTables.add(Tag.DSIG);
          // AAT tables, not yet defined in sfntly Tag class
          removeTables.add(Tag.intValue(new byte[]{'m', 'o', 'r', 't'}));
          removeTables.add(Tag.intValue(new byte[]{'m', 'o', 'r', 'x'}));
          subsetter.setRemoveTables(removeTables);
          newFont = subsetter.subset().build();
        }
        if (strip) {
          Subsetter hintStripper = new HintStripper(newFont, fontFactory);