org.apache.fontbox.ttf.TTFSubFont.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.fontbox.ttf.TTFSubFont.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.fontbox.ttf;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.fontbox.encoding.Encoding;
import org.apache.fontbox.encoding.MacRomanEncoding;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A font, which is comprised of a subset of characters of a TrueType font.
 * Based on code developed by Wolfgang Glas
 * http://svn.clazzes.org/svn/sketch/trunk/pdf/pdf-entities/src/main/java/org/clazzes/sketch/pdf/entities/impl/TTFSubFont.java
 */
public class TTFSubFont {

    private static final Log LOG = LogFactory.getLog(TTFSubFont.class);
    private static final byte[] PAD_BUF = new byte[] { 0, 0, 0 };

    private final TrueTypeFont baseTTF;
    private final String nameSuffix;
    private final CMAPEncodingEntry baseCmap;

    // A map of unicode char codes to glyph IDs of the original font.
    private final SortedMap<Integer, Integer> characters;
    // A sorted version of this set will comprise the generated glyph IDs
    // for the written truetype font.
    private final SortedSet<Integer> glyphIds;

    /**
     * Constructs a subfont based on the given font using the given suffix.
     * 
     * @param baseFont the base font of the subfont
     * @param suffix suffix used for the naming
     * 
     */
    public TTFSubFont(TrueTypeFont baseFont, String suffix) {
        baseTTF = baseFont;
        nameSuffix = suffix;
        characters = new TreeMap<Integer, Integer>();
        glyphIds = new TreeSet<Integer>();

        CMAPEncodingEntry[] cmaps = this.baseTTF.getCMAP().getCmaps();
        CMAPEncodingEntry unicodeCmap = null;

        for (CMAPEncodingEntry cmap : cmaps) {
            // take first unicode map.
            if (cmap.getPlatformId() == 0 || (cmap.getPlatformId() == 3 && cmap.getPlatformEncodingId() == 1)) {
                unicodeCmap = cmap;
                break;
            }
        }
        baseCmap = unicodeCmap;
        // add notdef character.
        addCharCode(0);
    }

    /**
     * Add the given charcode to the subfpont.
     * 
     * @param charCode the charCode to be added
     * 
     */
    public void addCharCode(int charCode) {
        Integer gid = Integer.valueOf(baseCmap.getGlyphId(charCode));
        if (charCode == 0 || gid != 0) {
            characters.put(charCode, gid);
            glyphIds.add(gid);
        }
    }

    private static int log2i(int i) {
        int ret = -1;
        if ((i & 0xffff0000) != 0) {
            i >>>= 16;
            ret += 16;
        }
        if ((i & 0xff00) != 0) {
            i >>>= 8;
            ret += 8;
        }
        if ((i & 0xf0) != 0) {
            i >>>= 4;
            ret += 4;
        }
        if ((i & 0xc) != 0) {
            i >>>= 2;
            ret += 2;
        }
        if ((i & 0x2) != 0) {
            i >>>= 1;
            ++ret;
        }
        if (i != 0) {
            ++ret;
        }
        return ret;
    }

    private static long buildUint32(int high, int low) {
        return ((((long) high) & 0xffffL) << 16) | (((long) low) & 0xffffL);
    }

    private static long buildUint32(byte[] bytes) {
        return ((((long) bytes[0]) & 0xffL) << 24) | ((((long) bytes[1]) & 0xffL) << 16)
                | ((((long) bytes[2]) & 0xffL) << 8) | (((long) bytes[3]) & 0xffL);
    }

    /**
     * @param dos The data output stream.
     * @param nTables The number of table.
     * @return The file offset of the first TTF table to write.
     * @throws IOException Upon errors.
     */
    private static long writeFileHeader(DataOutputStream dos, int nTables) throws IOException {
        dos.writeInt(0x00010000);
        dos.writeShort(nTables);

        int mask = Integer.highestOneBit(nTables);
        int searchRange = mask * 16;
        dos.writeShort(searchRange);

        int entrySelector = log2i(mask);

        dos.writeShort(entrySelector);

        // numTables * 16 - searchRange
        int last = 16 * nTables - searchRange;
        dos.writeShort(last);

        return 0x00010000L + buildUint32(nTables, searchRange) + buildUint32(entrySelector, last);
    }

    private static long writeTableHeader(DataOutputStream dos, String tag, long offset, byte[] bytes)
            throws IOException {

        int n = bytes.length;
        int nup;
        long checksum = 0L;

        for (nup = 0; nup < n; ++nup) {
            checksum += (((long) bytes[nup]) & 0xffL) << (24 - (nup % 4) * 8);
        }

        checksum &= 0xffffffffL;

        LOG.debug(String.format("Writing table header [%s,%08x,%08x,%08x]", tag, checksum, offset, bytes.length));

        byte[] tagbytes = tag.getBytes("US-ASCII");

        dos.write(tagbytes, 0, 4);
        dos.writeInt((int) checksum);
        dos.writeInt((int) offset);
        dos.writeInt(bytes.length);

        // account for the checksum twice, one time for the header field, on time for the content itself.
        return buildUint32(tagbytes) + checksum + checksum + offset + bytes.length;
    }

    private static void writeTableBody(OutputStream os, byte[] bytes) throws IOException {
        int n = bytes.length;
        os.write(bytes);
        if ((n % 4) != 0) {
            os.write(PAD_BUF, 0, 4 - n % 4);
        }
    }

    private static void writeFixed(DataOutputStream dos, double f) throws IOException {
        double ip = Math.floor(f);
        double fp = (f - ip) * 65536.0;
        dos.writeShort((int) ip);
        dos.writeShort((int) fp);
    }

    private static void writeUint32(DataOutputStream dos, long l) throws IOException {
        dos.writeInt((int) l);
    }

    private static void writeUint16(DataOutputStream dos, int i) throws IOException {
        dos.writeShort(i);
    }

    private static void writeSint16(DataOutputStream dos, short i) throws IOException {
        dos.writeShort(i);
    }

    private static void writeUint8(DataOutputStream dos, int i) throws IOException {
        dos.writeByte(i);
    }

    private static void writeLongDateTime(DataOutputStream dos, Calendar calendar) throws IOException {
        // inverse operation of TTFDataStream.readInternationalDate()
        GregorianCalendar cal = new GregorianCalendar(1904, 0, 1);
        long millisFor1904 = cal.getTimeInMillis();
        long secondsSince1904 = (calendar.getTimeInMillis() - millisFor1904) / 1000L;
        dos.writeLong(secondsSince1904);
    }

    private byte[] buildHeadTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        LOG.debug("Building table [head]...");

        HeaderTable h = this.baseTTF.getHeader();

        writeFixed(dos, h.getVersion());
        writeFixed(dos, h.getFontRevision());
        writeUint32(dos, 0 /* h.getCheckSumAdjustment() */);
        writeUint32(dos, h.getMagicNumber());
        writeUint16(dos, h.getFlags());
        writeUint16(dos, h.getUnitsPerEm());
        writeLongDateTime(dos, h.getCreated());
        writeLongDateTime(dos, h.getModified());
        writeSint16(dos, h.getXMin());
        writeSint16(dos, h.getYMin());
        writeSint16(dos, h.getXMax());
        writeSint16(dos, h.getYMax());
        writeUint16(dos, h.getMacStyle());
        writeUint16(dos, h.getLowestRecPPEM());
        writeSint16(dos, h.getFontDirectionHint());
        // force long format of 'loca' table.
        writeSint16(dos, (short) 1 /* h.getIndexToLocFormat() */);
        writeSint16(dos, h.getGlyphDataFormat());
        dos.flush();

        LOG.debug("Finished table [head].");

        return bos.toByteArray();
    }

    private byte[] buildHheaTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        LOG.debug("Building table [hhea]...");

        HorizontalHeaderTable h = this.baseTTF.getHorizontalHeader();

        writeFixed(dos, h.getVersion());
        writeSint16(dos, h.getAscender());
        writeSint16(dos, h.getDescender());
        writeSint16(dos, h.getLineGap());
        writeUint16(dos, h.getAdvanceWidthMax());
        writeSint16(dos, h.getMinLeftSideBearing());
        writeSint16(dos, h.getMinRightSideBearing());
        writeSint16(dos, h.getXMaxExtent());
        writeSint16(dos, h.getCaretSlopeRise());
        writeSint16(dos, h.getCaretSlopeRun());
        writeSint16(dos, h.getReserved1()); // caretOffset
        writeSint16(dos, h.getReserved2());
        writeSint16(dos, h.getReserved3());
        writeSint16(dos, h.getReserved4());
        writeSint16(dos, h.getReserved5());
        writeSint16(dos, h.getMetricDataFormat());
        writeUint16(dos, glyphIds.subSet(0, h.getNumberOfHMetrics()).size());

        dos.flush();
        LOG.debug("Finished table [hhea].");
        return bos.toByteArray();
    }

    private static boolean replicateNameRecord(NameRecord nr) {
        return nr.getPlatformId() == NameRecord.PLATFORM_WINDOWS
                && nr.getPlatformEncodingId() == NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE
                && nr.getLanguageId() == 0 && nr.getNameId() >= 0 && nr.getNameId() < 7;
    }

    private byte[] buildNameTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        LOG.debug("Building table [name]...");

        NamingTable n = this.baseTTF.getNaming();
        List<NameRecord> nameRecords = null;
        if (n != null) {
            nameRecords = n.getNameRecords();
        } else {
            // sometimes there is no naming table in an embedded subfonts
            // create some dummies
            nameRecords = new ArrayList<NameRecord>();
            NameRecord nr = new NameRecord();
            nr.setPlatformId(NameRecord.PLATFORM_WINDOWS);
            nr.setPlatformEncodingId(NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE);
            nr.setLanguageId(0);
            nr.setNameId(NameRecord.NAME_FONT_FAMILY_NAME);
            nr.setString("PDFBox-Dummy-Familyname");
            nameRecords.add(nr);
            nr = new NameRecord();
            nr.setPlatformId(NameRecord.PLATFORM_WINDOWS);
            nr.setPlatformEncodingId(NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE);
            nr.setLanguageId(0);
            nr.setNameId(NameRecord.NAME_FULL_FONT_NAME);
            nr.setString("PDFBox-Dummy-Fullname");
            nameRecords.add(nr);
        }
        int numberOfRecords = nameRecords.size();
        int nrep = 0;
        for (int i = 0; i < numberOfRecords; ++i) {
            NameRecord nr = nameRecords.get(i);
            if (replicateNameRecord(nr)) {
                LOG.debug("Writing name record [" + nr.getNameId() + "], [" + nr.getString() + "],");
                ++nrep;
            }
        }
        writeUint16(dos, 0);
        writeUint16(dos, nrep);
        writeUint16(dos, 2 * 3 + (2 * 6) * nrep);

        byte[][] names = new byte[nrep][];
        int j = 0;
        for (int i = 0; i < numberOfRecords; ++i) {
            NameRecord nr = nameRecords.get(i);
            if (replicateNameRecord(nr)) {
                int platform = nr.getPlatformId();
                int encoding = nr.getPlatformEncodingId();
                String charset = "ISO-8859-1";
                if (platform == 3 && encoding == 1) {
                    charset = "UTF-16BE";
                } else if (platform == 2) {
                    if (encoding == 0) {
                        charset = "US-ASCII";
                    } else if (encoding == 1) {
                        //not sure is this is correct??
                        charset = "UTF16-BE";
                    } else if (encoding == 2) {
                        charset = "ISO-8859-1";
                    }
                }
                String value = nr.getString();
                if (nr.getNameId() == 6 && this.nameSuffix != null) {
                    value += this.nameSuffix;
                }
                names[j] = value.getBytes(charset);
                ++j;
            }
        }

        int offset = 0;
        j = 0;
        for (int i = 0; i < numberOfRecords; ++i) {
            NameRecord nr = nameRecords.get(i);
            if (replicateNameRecord(nr)) {
                writeUint16(dos, nr.getPlatformId());
                writeUint16(dos, nr.getPlatformEncodingId());
                writeUint16(dos, nr.getLanguageId());
                writeUint16(dos, nr.getNameId());
                writeUint16(dos, names[j].length);
                writeUint16(dos, offset);
                offset += names[j].length;
                ++j;
            }
        }

        for (int i = 0; i < nrep; ++i) {
            dos.write(names[i]);
        }
        dos.flush();
        LOG.debug("Finished table [name].");
        return bos.toByteArray();
    }

    private byte[] buildMaxpTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        LOG.debug("Building table [maxp]...");

        MaximumProfileTable p = this.baseTTF.getMaximumProfile();

        writeFixed(dos, 1.0);
        writeUint16(dos, glyphIds.size());
        writeUint16(dos, p.getMaxPoints());
        writeUint16(dos, p.getMaxContours());
        writeUint16(dos, p.getMaxCompositePoints());
        writeUint16(dos, p.getMaxCompositeContours());
        writeUint16(dos, p.getMaxZones());
        writeUint16(dos, p.getMaxTwilightPoints());
        writeUint16(dos, p.getMaxStorage());
        writeUint16(dos, p.getMaxFunctionDefs());
        writeUint16(dos, 0);
        writeUint16(dos, p.getMaxStackElements());
        writeUint16(dos, 0);
        writeUint16(dos, p.getMaxComponentElements());
        writeUint16(dos, p.getMaxComponentDepth());

        dos.flush();
        LOG.debug("Finished table [maxp].");
        return bos.toByteArray();
    }

    private byte[] buildOS2Table() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        OS2WindowsMetricsTable os2 = this.baseTTF.getOS2Windows();
        if (os2 == null) {
            // sometimes there is no OS2 table in an embedded subfonts
            // create a dummy
            os2 = new OS2WindowsMetricsTable();
        }

        LOG.debug("Building table [OS/2]...");

        writeUint16(dos, 0);
        writeSint16(dos, os2.getAverageCharWidth());
        writeUint16(dos, os2.getWeightClass());
        writeUint16(dos, os2.getWidthClass());

        writeSint16(dos, os2.getFsType());

        writeSint16(dos, os2.getSubscriptXSize());
        writeSint16(dos, os2.getSubscriptYSize());
        writeSint16(dos, os2.getSubscriptXOffset());
        writeSint16(dos, os2.getSubscriptYOffset());

        writeSint16(dos, os2.getSuperscriptXSize());
        writeSint16(dos, os2.getSuperscriptYSize());
        writeSint16(dos, os2.getSuperscriptXOffset());
        writeSint16(dos, os2.getSuperscriptYOffset());

        writeSint16(dos, os2.getStrikeoutSize());
        writeSint16(dos, os2.getStrikeoutPosition());
        writeUint8(dos, os2.getFamilyClass());
        writeUint8(dos, os2.getFamilySubClass());
        dos.write(os2.getPanose());

        writeUint32(dos, 0);
        writeUint32(dos, 0);
        writeUint32(dos, 0);
        writeUint32(dos, 0);

        dos.write(os2.getAchVendId().getBytes("ISO-8859-1"));

        Iterator<Entry<Integer, Integer>> it = characters.entrySet().iterator();
        it.next();
        Entry<Integer, Integer> first = it.next();

        writeUint16(dos, os2.getFsSelection());
        writeUint16(dos, first.getKey());
        writeUint16(dos, characters.lastKey());
        /*
         * The mysterious Microsoft additions.
         *
         * SHORT    sTypoAscender    
         * SHORT    sTypoDescender    
         * SHORT    sTypoLineGap
         * USHORT    usWinAscent
         * USHORT    usWinDescent
         */
        writeUint16(dos, os2.getTypoAscender());
        writeUint16(dos, os2.getTypoDescender());
        writeUint16(dos, os2.getTypeLineGap());
        writeUint16(dos, os2.getWinAscent());
        writeUint16(dos, os2.getWinDescent());

        dos.flush();
        LOG.debug("Finished table [OS/2].");
        return bos.toByteArray();
    }

    private byte[] buildLocaTable(long[] newOffsets) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        LOG.debug("Building table [loca]...");

        for (long newOff : newOffsets) {
            writeUint32(dos, newOff);
        }
        dos.flush();
        LOG.debug("Finished table [loca].");
        return bos.toByteArray();
    }

    private boolean addCompoundReferences() throws IOException {
        GlyphTable g = this.baseTTF.getGlyph();
        long[] offsets = this.baseTTF.getIndexToLocation().getOffsets();
        InputStream is = this.baseTTF.getOriginalData();
        Set<Integer> glyphIdsToAdd = null;
        try {
            is.skip(g.getOffset());
            long lastOff = 0L;
            for (Integer glyphId : this.glyphIds) {
                long offset = offsets[glyphId.intValue()];
                long len = offsets[glyphId.intValue() + 1] - offset;
                is.skip(offset - lastOff);
                byte[] buf = new byte[(int) len];
                is.read(buf);
                // rewrite glyphIds for compound glyphs
                if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1) {
                    int off = 2 * 5;
                    int flags = 0;
                    do {
                        flags = ((((int) buf[off]) & 0xff) << 8) | (buf[off + 1] & 0xff);
                        off += 2;
                        int ogid = ((((int) buf[off]) & 0xff) << 8) | (buf[off + 1] & 0xff);
                        if (!this.glyphIds.contains(ogid)) {
                            LOG.debug("Adding referenced glyph " + ogid + " of compound glyph " + glyphId);
                            if (glyphIdsToAdd == null) {
                                glyphIdsToAdd = new TreeSet<Integer>();
                            }
                            glyphIdsToAdd.add(ogid);
                        }
                        off += 2;
                        // ARG_1_AND_2_ARE_WORDS
                        if ((flags & (1 << 0)) != 0) {
                            off += 2 * 2;
                        } else {
                            off += 2;
                        }
                        // WE_HAVE_A_TWO_BY_TWO
                        if ((flags & (1 << 7)) != 0) {
                            off += 2 * 4;
                        }
                        // WE_HAVE_AN_X_AND_Y_SCALE
                        else if ((flags & (1 << 6)) != 0) {
                            off += 2 * 2;
                        }
                        // WE_HAVE_A_SCALE
                        else if ((flags & (1 << 3)) != 0) {
                            off += 2;
                        }

                        // MORE_COMPONENTS
                    } while ((flags & (1 << 5)) != 0);

                }
                lastOff = offsets[glyphId.intValue() + 1];
            }
        } finally {
            is.close();
        }
        if (glyphIdsToAdd != null) {
            this.glyphIds.addAll(glyphIdsToAdd);
        }
        return glyphIdsToAdd == null;
    }

    private byte[] buildGlyfTable(long[] newOffsets) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        LOG.debug("Building table [glyf]...");
        GlyphTable g = this.baseTTF.getGlyph();
        long[] offsets = this.baseTTF.getIndexToLocation().getOffsets();
        InputStream is = this.baseTTF.getOriginalData();
        try {
            is.skip(g.getOffset());
            long lastOff = 0L;
            long newOff = 0L;
            int ioff = 0;
            for (Integer glyphId : this.glyphIds) {
                long offset = offsets[glyphId.intValue()];
                long len = offsets[glyphId.intValue() + 1] - offset;
                newOffsets[ioff++] = newOff;
                is.skip(offset - lastOff);
                byte[] buf = new byte[(int) len];
                is.read(buf);
                // rewrite glyphIds for compound glyphs
                if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1) {
                    LOG.debug("Compound glyph " + glyphId);
                    int off = 2 * 5;
                    int flags = 0;
                    do {
                        // clear the WE_HAVE_INSTRUCTIONS bit. (bit 8 is lsb of the first byte)
                        buf[off] &= 0xfe;
                        flags = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff);
                        off += 2;
                        int ogid = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff);
                        if (!this.glyphIds.contains(ogid)) {
                            this.glyphIds.add(ogid);
                        }
                        int ngid = this.getNewGlyphId(ogid);
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(String.format("mapped glyph  %d to %d in compound reference (flags=%04x)",
                                    ogid, ngid, flags));
                        }
                        buf[off] = (byte) (ngid >>> 8);
                        buf[off + 1] = (byte) ngid;
                        off += 2;
                        // ARG_1_AND_2_ARE_WORDS
                        if ((flags & (1 << 0)) != 0) {
                            off += 2 * 2;
                        } else {
                            off += 2;
                        }
                        // WE_HAVE_A_TWO_BY_TWO
                        if ((flags & (1 << 7)) != 0) {
                            off += 2 * 4;
                        }
                        // WE_HAVE_AN_X_AND_Y_SCALE
                        else if ((flags & (1 << 6)) != 0) {
                            off += 2 * 2;
                        }
                        // WE_HAVE_A_SCALE
                        else if ((flags & (1 << 3)) != 0) {
                            off += 2;
                        }
                        // MORE_COMPONENTS
                    } while ((flags & (1 << 5)) != 0);
                    // write the compound glyph
                    bos.write(buf, 0, off);
                    newOff += off;
                } else if (buf.length > 0) {
                    /*
                     * bail out instructions for simple glyphs, an excerpt from the specs is given below:
                     *                         
                     * int16    numberOfContours    If the number of contours is positive or zero, it is a single glyph;
                     * If the number of contours is -1, the glyph is compound
                     *  FWord    xMin    Minimum x for coordinate data
                     *  FWord    yMin    Minimum y for coordinate data
                     *  FWord    xMax    Maximum x for coordinate data
                     *  FWord    yMax    Maximum y for coordinate data
                     *  (here follow the data for the simple or compound glyph)
                     *
                     * Table 15: Simple glyph definition
                     *  Type    Name    Description
                     *  uint16  endPtsOfContours[n] Array of last points of each contour; n is the number of contours;
                     *          array entries are point indices
                     *  uint16  instructionLength Total number of bytes needed for instructions
                     *  uint8   instructions[instructionLength] Array of instructions for this glyph
                     *  uint8   flags[variable] Array of flags
                     *  uint8 or int16  xCoordinates[] Array of x-coordinates; the first is relative to (0,0),
                     *                                 others are relative to previous point
                     *  uint8 or int16  yCoordinates[] Array of y-coordinates; the first is relative to (0,0), 
                     *                                 others are relative to previous point
                     */

                    int numberOfContours = (((int) buf[0] & 0xff) << 8) | ((int) buf[1] & 0xff);

                    // offset of instructionLength
                    int off = 2 * 5 + numberOfContours * 2;

                    // write numberOfContours, xMin, yMin, xMax, yMax, endPtsOfContours[n]
                    bos.write(buf, 0, off);
                    newOff += off;

                    int instructionLength = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff);

                    // zarro instructions.
                    bos.write(0);
                    bos.write(0);
                    newOff += 2;

                    off += 2 + instructionLength;

                    // flags and coordinates
                    bos.write(buf, off, buf.length - off);
                    newOff += buf.length - off;
                }

                if ((newOff % 4L) != 0L) {
                    int np = (int) (4 - newOff % 4L);
                    bos.write(PAD_BUF, 0, np);
                    newOff += np;
                }

                lastOff = offsets[glyphId.intValue() + 1];
            }
            newOffsets[ioff++] = newOff;
        } finally {
            is.close();
        }
        LOG.debug("Finished table [glyf].");
        return bos.toByteArray();
    }

    private int getNewGlyphId(Integer oldGid) {
        return this.glyphIds.headSet(oldGid).size();
    }

    private byte[] buildCmapTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        LOG.debug("Building table [cmap]...");
        /*
         * UInt16    version    Version number (Set to zero)
         * UInt16    numberSubtables    Number of encoding subtables
         */
        writeUint16(dos, 0);
        writeUint16(dos, 1);
        /*
         * UInt16    platformID    Platform identifier
         * UInt16    platformSpecificID    Platform-specific encoding identifier
         * UInt32    offset    Offset of the mapping table
         */
        writeUint16(dos, 3); // unicode
        writeUint16(dos, 1); // Default Semantics
        writeUint32(dos, 4 * 2 + 4);
        // mapping of type 4.
        Iterator<Entry<Integer, Integer>> it = this.characters.entrySet().iterator();
        it.next();
        Entry<Integer, Integer> lastChar = it.next();
        Entry<Integer, Integer> prevChar = lastChar;
        int lastGid = this.getNewGlyphId(lastChar.getValue());

        int[] startCode = new int[this.characters.size()];
        int[] endCode = new int[this.characters.size()];
        int[] idDelta = new int[this.characters.size()];
        int nseg = 0;
        while (it.hasNext()) {
            Entry<Integer, Integer> curChar = it.next();
            int curGid = this.getNewGlyphId(curChar.getValue());

            if (curChar.getKey() != prevChar.getKey() + 1
                    || curGid - lastGid != curChar.getKey() - lastChar.getKey()) {
                // Don't emit ranges, which map to the undef glyph, the
                // undef glyph is emitted a the very last segment.
                if (lastGid != 0) {
                    startCode[nseg] = lastChar.getKey();
                    endCode[nseg] = prevChar.getKey();
                    idDelta[nseg] = lastGid - lastChar.getKey();
                    ++nseg;
                }
                // shorten ranges which start with undef by one.
                else if (!lastChar.getKey().equals(prevChar.getKey())) {
                    startCode[nseg] = lastChar.getKey() + 1;
                    endCode[nseg] = prevChar.getKey();
                    idDelta[nseg] = lastGid - lastChar.getKey();
                    ++nseg;
                }
                lastGid = curGid;
                lastChar = curChar;
            }
            prevChar = curChar;
        }
        // trailing segment
        startCode[nseg] = lastChar.getKey();
        endCode[nseg] = prevChar.getKey();
        idDelta[nseg] = lastGid - lastChar.getKey();
        ++nseg;
        // notdef character.
        startCode[nseg] = 0xffff;
        endCode[nseg] = 0xffff;
        idDelta[nseg] = 1;
        ++nseg;

        /*
         * UInt16    format    Format number is set to 4     
         * UInt16    length    Length of subtable in bytes     
         * UInt16    language    Language code for this encoding subtable, or zero if language-independent     
         * UInt16    segCountX2    2 * segCount     
         * UInt16    searchRange    2 * (2**FLOOR(log2(segCount)))     
         * UInt16    entrySelector    log2(searchRange/2)     
         * UInt16    rangeShift    (2 * segCount) - searchRange     
         * UInt16    endCode[segCount]    Ending character code for each segment, last = 0xFFFF.    
         * UInt16    reservedPad    This value should be zero    
         * UInt16    startCode[segCount]    Starting character code for each segment    
         * UInt16    idDelta[segCount]    Delta for all character codes in segment     
         * UInt16    idRangeOffset[segCount]    Offset in bytes to glyph indexArray, or 0     
         * UInt16    glyphIndexArray[variable]    Glyph index array
         */

        writeUint16(dos, 4);
        writeUint16(dos, 8 * 2 + nseg * (4 * 2));
        writeUint16(dos, 0);
        writeUint16(dos, nseg * 2);
        int nsegHigh = Integer.highestOneBit(nseg);
        writeUint16(dos, nsegHigh * 2);
        writeUint16(dos, log2i(nsegHigh));
        writeUint16(dos, 2 * (nseg - nsegHigh));

        for (int i = 0; i < nseg; ++i) {
            writeUint16(dos, endCode[i]);
        }
        writeUint16(dos, 0);
        for (int i = 0; i < nseg; ++i) {
            writeUint16(dos, startCode[i]);
        }
        for (int i = 0; i < nseg; ++i) {
            writeUint16(dos, idDelta[i]);
        }
        for (int i = 0; i < nseg; ++i) {
            writeUint16(dos, 0);
        }
        LOG.debug("Finished table [cmap].");
        return bos.toByteArray();
    }

    private byte[] buildPostTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        LOG.debug("Building table [post]...");
        PostScriptTable p = this.baseTTF.getPostScript();
        if (p == null) {
            // sometimes there is no post table in an embedded subfonts
            // create a dummy
            p = new PostScriptTable();
        }
        String[] glyphNames = p.getGlyphNames();
        /*
        Fixed    format    Format of this table
        Fixed    italicAngle    Italic angle in degrees
        FWord    underlinePosition    Underline position
        FWord    underlineThickness    Underline thickness
        uint32    isFixedPitch    Font is monospaced; set to 1 if the font is monospaced and 0 otherwise 
        (N.B., to maintain compatibility with older versions of the TrueType spec, accept any non-zero value
         as meaning that the font is monospaced)
        uint32    minMemType42    Minimum memory usage when a TrueType font is downloaded as a Type 42 font
        uint32    maxMemType42    Maximum memory usage when a TrueType font is downloaded as a Type 42 font
        uint32    minMemType1    Minimum memory usage when a TrueType font is downloaded as a Type 1 font
        uint32    maxMemType1    Maximum memory usage when a TrueType font is downloaded as a Type 1 font
        uint16    numberOfGlyphs    number of glyphs
        uint16    glyphNameIndex[numberOfGlyphs]    Ordinal number of this glyph in 'post' string tables. 
        This is not an offset.
        Pascal string    names[numberNewGlyphs]  glyph names with length bytes [variable] (a Pascal string)
         */
        writeFixed(dos, 2.0);
        writeFixed(dos, p.getItalicAngle());
        writeSint16(dos, p.getUnderlinePosition());
        writeSint16(dos, p.getUnderlineThickness());
        writeUint32(dos, p.getIsFixedPitch());
        writeUint32(dos, p.getMinMemType42());
        writeUint32(dos, p.getMaxMemType42());
        writeUint32(dos, p.getMimMemType1());
        writeUint32(dos, p.getMaxMemType1());
        writeUint16(dos, baseTTF.getHorizontalHeader().getNumberOfHMetrics());

        List<String> additionalNames = new ArrayList<String>();
        Map<String, Integer> additionalNamesIndices = new HashMap<String, Integer>();

        if (glyphNames == null) {
            Encoding enc = MacRomanEncoding.INSTANCE;
            int[] gidToUC = this.baseCmap.getGlyphIdToCharacterCode();
            for (Integer glyphId : this.glyphIds) {
                int uc = gidToUC[glyphId.intValue()];
                String name = null;
                if (uc < 0x8000) {
                    try {
                        name = enc.getNameFromCharacter((char) uc);
                    } catch (IOException e) {
                        // TODO
                    }
                }
                if (name == null) {
                    name = String.format(Locale.ENGLISH, "uni%04X", uc);
                }
                Integer macId = Encoding.MAC_GLYPH_NAMES_INDICES.get(name);
                if (macId == null) {
                    Integer idx = additionalNamesIndices.get(name);
                    if (idx == null) {
                        idx = additionalNames.size();
                        additionalNames.add(name);
                        additionalNamesIndices.put(name, idx);
                    }
                    writeUint16(dos, idx.intValue() + 258);
                } else {
                    writeUint16(dos, macId.intValue());
                }
            }
        } else {
            for (Integer glyphId : this.glyphIds) {
                String name = glyphNames[glyphId.intValue()];
                Integer macId = Encoding.MAC_GLYPH_NAMES_INDICES.get(name);
                if (macId == null) {
                    Integer idx = additionalNamesIndices.get(name);
                    if (idx == null) {
                        idx = additionalNames.size();
                        additionalNames.add(name);
                        additionalNamesIndices.put(name, idx);
                    }
                    writeUint16(dos, idx.intValue() + 258);
                } else {
                    writeUint16(dos, macId.intValue());
                }
            }
        }

        for (String name : additionalNames) {
            LOG.debug("additionalName=[" + name + "].");
            byte[] buf = name.getBytes("US-ASCII");
            writeUint8(dos, buf.length);
            dos.write(buf);
        }
        dos.flush();
        LOG.debug("Finished table [post].");
        return bos.toByteArray();
    }

    private byte[] buildHmtxTable() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        LOG.debug("Building table [hmtx]...");
        HorizontalHeaderTable h = this.baseTTF.getHorizontalHeader();
        HorizontalMetricsTable hm = this.baseTTF.getHorizontalMetrics();
        byte[] buf = new byte[4];
        InputStream is = this.baseTTF.getOriginalData();
        try {
            is.skip(hm.getOffset());
            long lastOff = 0;
            for (Integer glyphId : this.glyphIds) {
                // offset in original file.
                long off;
                if (glyphId < h.getNumberOfHMetrics()) {
                    off = glyphId * 4;
                } else {
                    off = h.getNumberOfHMetrics() * 4 + (glyphId - h.getNumberOfHMetrics()) * 2;
                }
                // skip over from last original offset.
                if (off != lastOff) {
                    long nskip = off - lastOff;
                    if (nskip != is.skip(nskip)) {
                        throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table.");
                    }
                }
                // read left side bearings only, if we are beyond numOfHMetrics.
                int n = glyphId < h.getNumberOfHMetrics() ? 4 : 2;
                if (n != is.read(buf, 0, n)) {
                    throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table.");
                }
                bos.write(buf, 0, n);
                lastOff = off + n;
            }
            LOG.debug("Finished table [hmtx].");
            return bos.toByteArray();
        } finally {
            is.close();
        }
    }

    /**
     * Write the subfont to the given output stream.
     * 
     * @param os the stream used for writing
     * @throws IOException if something went wrong.
     */
    public void writeToStream(OutputStream os) throws IOException {
        LOG.debug("glyphIds=[" + glyphIds + "]");
        LOG.debug("numGlyphs=[" + glyphIds.size() + "]");
        while (!addCompoundReferences()) {
        }
        DataOutputStream dos = new DataOutputStream(os);
        try {
            /*
            'cmap'    character to glyph mapping
            'glyf'    glyph data
            'head'    font header
            'hhea'    horizontal header
            'OS/2'  OS/2 compatibility table.
            'hmtx'    horizontal metrics
            'loca'    index to location
            'maxp'    maximum profile
            'name'    naming
            'post'    PostScript
             */
            String[] tableNames = { "OS/2", "cmap", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "name",
                    "post" };
            byte[][] tables = new byte[tableNames.length][];
            long[] newOffsets = new long[this.glyphIds.size() + 1];
            tables[3] = this.buildHeadTable();
            tables[4] = this.buildHheaTable();
            tables[7] = this.buildMaxpTable();
            tables[8] = this.buildNameTable();
            tables[0] = this.buildOS2Table();
            tables[2] = this.buildGlyfTable(newOffsets);
            tables[6] = this.buildLocaTable(newOffsets);
            tables[1] = this.buildCmapTable();
            tables[5] = this.buildHmtxTable();
            tables[9] = this.buildPostTable();
            long checksum = writeFileHeader(dos, tableNames.length);
            long offset = 12L + 16L * tableNames.length;
            for (int i = 0; i < tableNames.length; ++i) {
                checksum += writeTableHeader(dos, tableNames[i], offset, tables[i]);
                offset += ((tables[i].length + 3) / 4) * 4;
            }
            checksum = 0xB1B0AFBAL - (checksum & 0xffffffffL);
            // correct checksumAdjustment of 'head' table.
            tables[3][8] = (byte) (checksum >>> 24);
            tables[3][9] = (byte) (checksum >>> 16);
            tables[3][10] = (byte) (checksum >>> 8);
            tables[3][11] = (byte) (checksum);
            for (int i = 0; i < tableNames.length; ++i) {
                writeTableBody(dos, tables[i]);
            }
        } finally {
            dos.close();
        }
    }
}