<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Pdf
 * @subpackage FileParser
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */

/** Zend_Pdf_FileParser_Font */
require_once 'Zend/Pdf/FileParser/Font.php';

/**
 * Abstract base class for OpenType font file parsers.
 *
 * TrueType was originally developed by Apple and was adopted as the default
 * font format for the Microsoft Windows platform. OpenType is an extension of
 * TrueType, developed jointly by Microsoft and Adobe, which adds support for
 * PostScript font data.
 *
 * This abstract parser class forms the foundation for concrete subclasses which
 * extract either TrueType or PostScript font data from the file.
 *
 * All OpenType files use big-endian byte ordering.
 *
 * The full TrueType and OpenType specifications can be found at:
 * <ul>
 *  <li>{@link http://developer.apple.com/textfonts/TTRefMan/}
 *  <li>{@link http://www.microsoft.com/typography/OTSPEC/}
 *  <li>{@link http://partners.adobe.com/public/developer/opentype/index_spec.html}
 * </ul>
 *
 * @package    Zend_Pdf
 * @subpackage FileParser
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Pdf_FileParser_Font_OpenType extends Zend_Pdf_FileParser_Font
{
  /**** Instance Variables ****/


    /**
     * Stores the scaler type (font type) for the font file. See
     * {@link _readScalerType()}.
     * @var integer
     */
    protected $_scalerType = 0;

    /**
     * Stores the byte offsets to the various information tables.
     * @var array
     */
    protected $_tableDirectory = array();



  /**** Public Interface ****/


  /* Semi-Concrete Class Implementation */

    /**
     * Verifies that the font file is in the expected format.
     *
     * NOTE: This method should be overridden in subclasses to check the
     * specific format and set $this->_isScreened!
     *
     * @throws Zend_Pdf_Exception
     */
    public function screen()
    {
        if ($this->_isScreened) {
            return;
        }
        $this->_readScalerType();
    }

    /**
     * Reads and parses the font data from the file on disk.
     *
     * NOTE: This method should be overridden in subclasses to add type-
     * specific parsing and set $this->isParsed.
     *
     * @throws Zend_Pdf_Exception
     */
    public function parse()
    {
        if ($this->_isParsed) {
            return;
        }

        /* Screen the font file first, if it hasn't been done yet.
        */
        $this->screen();

        /* Start by reading the table directory.
         */
        $this->_parseTableDirectory();

        /* Then parse all of the required tables.
         */
        $this->_parseHeadTable();
        $this->_parseNameTable();
        $this->_parsePostTable();
        $this->_parseHheaTable();
        $this->_parseMaxpTable();
        $this->_parseOs2Table();
        $this->_parseHmtxTable();
        $this->_parseCmapTable();

        /* If present, parse the optional tables.
         */
        /**
         * @todo Add parser for kerning pairs.
         * @todo Add parser for ligatures.
         * @todo Add parser for other useful hinting tables.
         */
    }



  /**** Internal Methods ****/


  /* Parser Methods */

    /**
     * Parses the OpenType table directory.
     *
     * The table directory contains the identifier, checksum, byte offset, and
     * length of each of the information tables housed in the font file.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseTableDirectory()
    {
        $this->moveToOffset(4);

        $tableCount = $this->readUInt(2);
        $this->_debugLog('%d tables', $tableCount);

        /* Sanity check, in case we're not actually reading a OpenType file and
         * the first four bytes coincidentally matched an OpenType signature in
         * screen() above.
         *
         * There are at minimum 7 required tables: cmap, head, hhea, hmtx, maxp,
         * name, and post. In the current OpenType standard, only 32 table types
         * are defined, so use 50 as a practical limit.
         */
        if (($tableCount < 7) || ($tableCount > 50)) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Table count not within expected range',
                                         Zend_Pdf_Exception::BAD_TABLE_COUNT);
        }

        /* Skip the next 6 bytes, which contain values to aid a binary search.
         */
        $this->skipBytes(6);

        /* The directory contains four values: the name of the table, checksum,
         * offset to the table from the beginning of the font, and actual data
         * length of the table.
         */
        for ($tableIndex = 0; $tableIndex < $tableCount; $tableIndex++) {
            $tableName = $this->readBytes(4);

            /* We ignore the checksum here for two reasons: First, the PDF viewer
             * will do this later anyway; Second, calculating the checksum would
             * require unsigned integers, which PHP does not currently provide.
             * We may revisit this in the future.
             */
            $this->skipBytes(4);

            $tableOffset = $this->readUInt(4);
            $tableLength = $this->readUInt(4);
            $this->_debugLog('%s offset: 0x%x; length: %d', $tableName, $tableOffset, $tableLength);

            /* Sanity checks for offset and length values.
             */
            $fileSize = $this->_dataSource->getSize();
            if (($tableOffset < 0) || ($tableOffset > $fileSize)) {
                require_once 'Zend/Pdf/Exception.php';
                throw new Zend_Pdf_Exception("Table offset ($tableOffset) not within expected range",
                                             Zend_Pdf_Exception::INDEX_OUT_OF_RANGE);
            }
            if (($tableLength < 0) || (($tableOffset + $tableLength) > $fileSize)) {
                require_once 'Zend/Pdf/Exception.php';
                throw new Zend_Pdf_Exception("Table length ($tableLength) not within expected range",
                                             Zend_Pdf_Exception::INDEX_OUT_OF_RANGE);
            }

            $this->_tableDirectory[$tableName]['offset'] = $tableOffset;
            $this->_tableDirectory[$tableName]['length'] = $tableLength;
        }
    }


    /**
     * Parses the OpenType head (Font Header) table.
     *
     * The head table contains global information about the font such as the
     * revision number and global metrics.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseHeadTable()
    {
        $this->_jumpToTable('head');

        /* We can read any version 1 table.
         */
        $tableVersion = $this->_readTableVersion(1, 1);

        /* Skip the font revision number and checksum adjustment.
         */
        $this->skipBytes(8);

        $magicNumber = $this->readUInt(4);
        if ($magicNumber != 0x5f0f3cf5) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Wrong magic number. Expected: 0x5f0f3cf5; actual: '
                                       . sprintf('%x', $magicNumber),
                                         Zend_Pdf_Exception::BAD_MAGIC_NUMBER);
        }

        /* Most of the flags we ignore, but there are a few values that are
         * useful for our layout routines.
         */
        $flags = $this->readUInt(2);
        $this->baselineAtZero    = $this->isBitSet(0, $flags);
        $this->useIntegerScaling = $this->isBitSet(3, $flags);

        $this->unitsPerEm = $this->readUInt(2);
        $this->_debugLog('Units per em: %d', $this->unitsPerEm);

        /* Skip creation and modification date/time.
         */
        $this->skipBytes(16);

        $this->xMin = $this->readInt(2);
        $this->yMin = $this->readInt(2);
        $this->xMax = $this->readInt(2);
        $this->yMax = $this->readInt(2);
        $this->_debugLog('Font bounding box: %d %d %d %d',
                         $this->xMin, $this->yMin, $this->xMax, $this->yMax);

        /* The style bits here must match the fsSelection bits in the OS/2
         * table, if present.
         */
        $macStyleBits = $this->readUInt(2);
        $this->isBold   = $this->isBitSet(0, $macStyleBits);
        $this->isItalic = $this->isBitSet(1, $macStyleBits);

        /* We don't need the remainder of data in this table: smallest readable
         * size, font direction hint, indexToLocFormat, and glyphDataFormat.
         */
    }


    /**
     * Parses the OpenType name (Naming) table.
     *
     * The name table contains all of the identifying strings associated with
     * the font such as its name, copyright, trademark, license, etc.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseNameTable()
    {
        $this->_jumpToTable('name');
        $baseOffset = $this->_tableDirectory['name']['offset'];

        /* The name table begins with a short header, followed by each of the
         * fixed-length name records, followed by the variable-length strings.
         */

        /* We only understand version 0 tables.
         */
        $tableFormat = $this->readUInt(2);
        if ($tableFormat != 0) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Unable to read format $tableFormat table",
                                         Zend_Pdf_Exception::DONT_UNDERSTAND_TABLE_VERSION);
        }
        $this->_debugLog('Format %d table', $tableFormat);

        $nameCount = $this->readUInt(2);
        $this->_debugLog('%d name strings', $nameCount);

        $storageOffset = $this->readUInt(2) + $baseOffset;
        $this->_debugLog('Storage offset: 0x%x', $storageOffset);

        /* Scan the name records for those we're interested in. We'll skip over
         * encodings and languages we don't understand or support. Prefer the
         * Microsoft Unicode encoding for a given name/language combination, but
         * use Mac Roman if nothing else is available. We will extract the
         * actual strings later.
         */
        $nameRecords = array();
        for ($nameIndex = 0; $nameIndex < $nameCount; $nameIndex++) {

            $platformID = $this->readUInt(2);
            $encodingID = $this->readUInt(2);

            if (! ( (($platformID == 3) && ($encodingID == 1)) ||    // Microsoft Unicode
                    (($platformID == 1) && ($encodingID == 0))       // Mac Roman
                   ) ) {
                $this->skipBytes(8);    // Not a supported encoding. Move on.
                continue;
            }

            $languageID = $this->readUInt(2);
            $nameID     = $this->readUInt(2);
            $nameLength = $this->readUInt(2);
            $nameOffset = $this->readUInt(2);

            $languageCode = $this->_languageCodeForPlatform($platformID, $languageID);
            if ($languageCode === null) {
                $this->_debugLog('Skipping languageID: 0x%x; platformID %d', $languageID, $platformID);
                continue;    // Not a supported language. Move on.
            }

            $this->_debugLog('Adding nameID: %d; languageID: 0x%x; platformID: %d; offset: 0x%x (0x%x); length: %d',
                             $nameID, $languageID, $platformID, $baseOffset + $nameOffset, $nameOffset, $nameLength);

            /* Entries in the name table are sorted by platform ID. If an entry
             * exists for both Mac Roman and Microsoft Unicode, the Unicode entry
             * will prevail since it is processed last.
             */
            $nameRecords[$nameID][$languageCode] = array('platform' => $platformID,
                                                         'offset'   => $nameOffset,
                                                         'length'   => $nameLength );
        }

        /* Now go back and extract the interesting strings.
         */
        $fontNames = array();
        foreach ($nameRecords as $name => $languages) {
            foreach ($languages as $language => $attributes) {
                $stringOffset = $storageOffset + $attributes['offset'];
                $this->moveToOffset($stringOffset);
                if ($attributes['platform'] == 3) {
                    $string = $this->readStringUTF16($attributes['length']);
                } else {
                    $string = $this->readStringMacRoman($attributes['length']);
                }
                $fontNames[$name][$language] = $string;
            }
        }

        $this->names = $fontNames;
    }


    /**
     * Parses the OpenType post (PostScript Information) table.
     *
     * The post table contains additional information required for using the font
     * on PostScript printers. It also contains the preferred location and
     * thickness for an underline, which is used by our layout code.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parsePostTable()
    {
        $this->_jumpToTable('post');

        /* We can read versions 1-4 tables.
         */
        $tableVersion = $this->_readTableVersion(1, 4);

        $this->italicAngle = $this->readFixed(16, 16);

        $this->underlinePosition = $this->readInt(2);
        $this->underlineThickness = $this->readInt(2);

        $fixedPitch = $this->readUInt(4);
        $this->isMonospaced = ($fixedPitch !== 0);

        /* Skip over PostScript virtual memory usage.
         */
        $this->skipBytes(16);

        /* The format of the remainder of this table is dependent on the table
         * version. However, since it contains glyph ordering information and
         * PostScript names which we don't use, move on. (This may change at
         * some point in the future though...)
         */
    }


    /**
     * Parses the OpenType hhea (Horizontal Header) table.
     *
     * The hhea table contains information used for horizontal layout. It also
     * contains some vertical layout information for Apple systems. The vertical
     * layout information for the PDF file is usually taken from the OS/2 table.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseHheaTable()
    {
        $this->_jumpToTable('hhea');

        /* We can read any version 1 table.
         */
        $tableVersion = $this->_readTableVersion(1, 1);

        /* The typographic ascent, descent, and line gap values are Apple-
         * specific. Similar values exist in the OS/2 table. We'll use these
         * values unless better values are found in OS/2.
         */
        $this->ascent = $this->readInt(2);
        $this->descent = $this->readInt(2);
        $this->lineGap = $this->readInt(2);

        /* The descent value is supposed to be negative--it's the distance
         * relative to the baseline. However, some fonts improperly store a
         * positive value in this field. If a positive value is found, flip the
         * sign and record a warning in the debug log that we did this.
         */
        if ($this->descent > 0) {
            $this->_debugLog('Warning: Font should specify negative descent. Actual: %d; Using %d',
                             $this->descent, -$this->descent);
            $this->descent = -$this->descent;
        }

        /* Skip over advance width, left and right sidebearing, max x extent,
         * caret slope rise, run, and offset, and the four reserved fields.
         */
        $this->skipBytes(22);

        /* These values are needed to read the hmtx table.
         */
        $this->metricDataFormat = $this->readInt(2);
        $this->numberHMetrics = $this->readUInt(2);
        $this->_debugLog('hmtx data format: %d; number of metrics: %d',
                         $this->metricDataFormat, $this->numberHMetrics);
    }


    /**
     * Parses the OpenType hhea (Horizontal Header) table.
     *
     * The hhea table contains information used for horizontal layout. It also
     * contains some vertical layout information for Apple systems. The vertical
     * layout information for the PDF file is usually taken from the OS/2 table.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseMaxpTable()
    {
        $this->_jumpToTable('maxp');

        /* We don't care about table version.
         */
        $this->_readTableVersion(0, 1);

        /* The number of glyphs in the font.
         */
        $this->numGlyphs = $this->readUInt(2);
        $this->_debugLog('number of glyphs: %d', $this->numGlyphs);

        // Skip other maxp table entries (if presented with table version 1.0)...
    }


    /**
     * Parses the OpenType OS/2 (OS/2 and Windows Metrics) table.
     *
     * The OS/2 table contains additional metrics data that is required to use
     * the font on the OS/2 or Microsoft Windows platforms. It is not required
     * for Macintosh fonts, so may not always be present. When available, we use
     * this table to determine most of the vertical layout and stylistic
     * information and for the font.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseOs2Table()
    {
        if (! $this->numberHMetrics) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("hhea table must be parsed prior to calling this method",
                                         Zend_Pdf_Exception::PARSED_OUT_OF_ORDER);
        }

        try {
            $this->_jumpToTable('OS/2');
        } catch (Zend_Pdf_Exception $e) {
            /* This table is not always present. If missing, use default values.
             */
            require_once 'Zend/Pdf/Exception.php';
            if ($e->getCode() == Zend_Pdf_Exception::REQUIRED_TABLE_NOT_FOUND) {
                $this->_debugLog('No OS/2 table found. Using default values');
                $this->fontWeight = Zend_Pdf_Font::WEIGHT_NORMAL;
                $this->fontWidth = Zend_Pdf_Font::WIDTH_NORMAL;
                $this->isEmbeddable = true;
                $this->isSubsettable = true;
                $this->strikeThickness = $this->unitsPerEm * 0.05;
                $this->strikePosition  = $this->unitsPerEm * 0.225;
                $this->isSerifFont      = false;    // the style of the font is unknown
                $this->isSansSerifFont  = false;
                $this->isOrnamentalFont = false;
                $this->isScriptFont     = false;
                $this->isSymbolicFont   = false;
                $this->isAdobeLatinSubset = false;
                $this->vendorID = '';
                $this->xHeight = 0;
                $this->capitalHeight = 0;
                return;
            } else {
                /* Something else went wrong. Throw this exception higher up the chain.
                 */
                throw $e;
                throw new Zend_Pdf_Exception($e->getMessage(), $e->getCode(), $e);
            }
        }

        /* Version 0 tables are becoming rarer these days. They are only found
         * in older fonts.
         *
         * Version 1 formally defines the Unicode character range bits and adds
         * two new fields to the end of the table.
         *
         * Version 2 defines several additional flags to the embedding bits
         * (fsType field) and five new fields to the end of the table.
         *
         * Versions 2 and 3 are structurally identical. There are only two
         * significant differences between the two: First, in version 3, the
         * average character width (xAvgCharWidth field) is calculated using all
         * non-zero width glyphs in the font instead of just the Latin lower-
         * case alphabetic characters; this doesn't affect us. Second, in
         * version 3, the embedding bits (fsType field) have been made mutually
         * exclusive; see additional discusson on this below.
         *
         * We can understand all four of these table versions.
         */
        $tableVersion = $this->readUInt(2);
        if (($tableVersion < 0) || ($tableVersion > 3)) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Unable to read version $tableVersion table",
                                         Zend_Pdf_Exception::DONT_UNDERSTAND_TABLE_VERSION);
        }
        $this->_debugLog('Version %d table', $tableVersion);

        $this->averageCharWidth = $this->readInt(2);

        /* Indicates the visual weight and aspect ratio of the characters. Used
         * primarily to logically sort fonts in lists. Also used to help choose
         * a more appropriate substitute font when necessary. See the WEIGHT_
         * and WIDTH_ constants defined in Zend_Pdf_Font.
         */
        $this->fontWeight = $this->readUInt(2);
        $this->fontWidth  = $this->readUInt(2);

        /* Describes the font embedding licensing rights. We can only embed and
         * subset a font when given explicit permission.
         *
         * NOTE: We always interpret these bits according to the rules defined
         * in version 3 of this table, regardless of the actual version. This
         * means we will perform our checks in order from the most-restrictive
         * to the least.
         */
        $embeddingFlags = $this->readUInt(2);
        $this->_debugLog('Embedding flags: %d', $embeddingFlags);
        if ($this->isBitSet(9, $embeddingFlags)) {
            /* Only bitmaps may be embedded. We don't have the ability to strip
             * outlines from fonts yet, so this means no embed.
             */
            $this->isEmbeddable = false;
        } elseif ($this->isBitSet(2, $embeddingFlags)
                || $this->isBitSet(3, $embeddingFlags)
                || $this->isBitSet(4, $embeddingFlags)
            ) {
                /* One of:
                 *     Restricted License embedding (0x0002)
                 *     Preview & Print embedding (0x0004)
                 *     Editable embedding (0x0008)
                 * is set.
                 */
                $this->isEmbeddable = true;
        } elseif ($this->isBitSet(1, $embeddingFlags)) {
                /* Restricted license embedding & no other embedding is set.
                 * We currently don't have any way to
                 * enforce this, so interpret this as no embed. This may be revised
                 * in the future...
                 */
                $this->isEmbeddable = false;
        } else {
            /* The remainder of the bit settings grant us permission to embed
             * the font. There may be additional usage rights granted or denied
             * but those only affect the PDF viewer application, not our code.
             */
            $this->isEmbeddable = true;
        }
        $this->_debugLog('Font ' . ($this->isEmbeddable ? 'may' : 'may not') . ' be embedded');
        $isSubsettable = $this->isBitSet($embeddingFlags, 8);

        /* Recommended size and offset for synthesized subscript characters.
         */
        $this->subscriptXSize = $this->readInt(2);
        $this->subscriptYSize = $this->readInt(2);
        $this->subscriptXOffset = $this->readInt(2);
        $this->subscriptYOffset = $this->readInt(2);

        /* Recommended size and offset for synthesized superscript characters.
         */
        $this->superscriptXSize = $this->readInt(2);
        $this->superscriptYSize = $this->readInt(2);
        $this->superscriptXOffset = $this->readInt(2);
        $this->superscriptYOffset = $this->readInt(2);

        /* Size and vertical offset for the strikethrough.
         */
        $this->strikeThickness = $this->readInt(2);
        $this->strikePosition  = $this->readInt(2);

        /* Describes the class of font: serif, sans serif, script. etc. These
         * values are defined here:
         *   http://www.microsoft.com/OpenType/OTSpec/ibmfc.htm
         */
        $familyClass = ($this->readUInt(2) >> 8);    // don't care about subclass
        $this->_debugLog('Font family class: %d', $familyClass);
        $this->isSerifFont      = ((($familyClass >= 1) && ($familyClass <= 5)) ||
                                   ($familyClass == 7));
        $this->isSansSerifFont  = ($familyClass == 8);
        $this->isOrnamentalFont = ($familyClass == 9);
        $this->isScriptFont     = ($familyClass == 10);
        $this->isSymbolicFont   = ($familyClass == 12);

        /* Skip over the PANOSE number. The interesting values for us overlap
         * with the font family class defined above.
         */
        $this->skipBytes(10);

        /* The Unicode range is made up of four 4-byte unsigned long integers
         * which are used as bitfields covering a 128-bit range. Each bit
         * represents a Unicode code block. If the bit is set, this font at
         * least partially covers the characters in that block.
         */
        $unicodeRange1 = $this->readUInt(4);
        $unicodeRange2 = $this->readUInt(4);
        $unicodeRange3 = $this->readUInt(4);
        $unicodeRange4 = $this->readUInt(4);
        $this->_debugLog('Unicode ranges: 0x%x 0x%x 0x%x 0x%x',
                        $unicodeRange1, $unicodeRange2, $unicodeRange3, $unicodeRange4);

        /* The Unicode range is currently only used to decide if the character
         * set covered by the font is a subset of the Adobe Latin set, meaning
         * it only has the basic latin set. If it covers any other characters,
         * even any of the extended latin characters, it is considered symbolic
         * to PDF and must be described differently in the Font Descriptor.
         */
        /**
         * @todo Font is recognized as Adobe Latin subset font if it only contains
         * Basic Latin characters (only bit 0 of Unicode range bits is set).
         * Actually, other Unicode subranges like General Punctuation (bit 31) also
         * fall into Adobe Latin characters. So this code has to be modified.
         */
        $this->isAdobeLatinSubset = (($unicodeRange1 == 1) && ($unicodeRange2 == 0) &&
                                      ($unicodeRange3 == 0) && ($unicodeRange4 == 0));
        $this->_debugLog(($this->isAdobeLatinSubset ? 'Is' : 'Is not') . ' a subset of Adobe Latin');

        $this->vendorID = $this->readBytes(4);

        /* Skip the font style bits. We use the values found in the 'head' table.
         * Also skip the first Unicode and last Unicode character indicies. Our
         * cmap implementation does not need these values.
         */
        $this->skipBytes(6);

        /* Typographic ascender, descender, and line gap. These values are
         * preferred to those in the 'hhea' table.
         */
        $this->ascent = $this->readInt(2);
        $this->descent = $this->readInt(2);
        $this->lineGap = $this->readInt(2);

        /* The descent value is supposed to be negative--it's the distance
         * relative to the baseline. However, some fonts improperly store a
         * positive value in this field. If a positive value is found, flip the
         * sign and record a warning in the debug log that we did this.
         */
        if ($this->descent > 0) {
            $this->_debugLog('Warning: Font should specify negative descent. Actual: %d; Using %d',
                             $this->descent, -$this->descent);
            $this->descent = -$this->descent;
        }

        /* Skip over Windows-specific ascent and descent.
         */
        $this->skipBytes(4);

        /* Versions 0 and 1 tables do not contain the x or capital height
         * fields. Record zero for unknown.
         */
        if ($tableVersion < 2) {
            $this->xHeight = 0;
            $this->capitalHeight = 0;
        } else {

            /* Skip over the Windows code page coverages. We are only concerned
             * with Unicode coverage.
             */
            $this->skipBytes(8);

            $this->xHeight = $this->readInt(2);
            $this->capitalHeight = $this->readInt(2);

            /* Ignore the remaining fields in this table. They are Windows-specific.
             */
        }
        /**
         * @todo Obtain the x and capital heights from the 'glyf' table if they
         *   haven't been supplied here instead of storing zero.
         */
    }


    /**
     * Parses the OpenType hmtx (Horizontal Metrics) table.
     *
     * The hmtx table contains the horizontal metrics for every glyph contained
     * within the font. These are the critical values for horizontal layout of
     * text.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseHmtxTable()
    {
        $this->_jumpToTable('hmtx');

        if (! $this->numberHMetrics) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("hhea table must be parsed prior to calling this method",
                                         Zend_Pdf_Exception::PARSED_OUT_OF_ORDER);
        }

        /* We only understand version 0 tables.
         */
        if ($this->metricDataFormat != 0) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Unable to read format $this->metricDataFormat table.",
                                         Zend_Pdf_Exception::DONT_UNDERSTAND_TABLE_VERSION);
        }

        /* The hmtx table has no header. For each glpyh in the font, it contains
         * the glyph's advance width and its left side bearing. We don't use the
         * left side bearing.
         */
        $glyphWidths = array();
        for ($i = 0; $i < $this->numberHMetrics; $i++) {
            $glyphWidths[$i] = $this->readUInt(2);
            $this->skipBytes(2);
        }
        /* Populate last value for the rest of array
         */
        while (count($glyphWidths) < $this->numGlyphs) {
            $glyphWidths[] = end($glyphWidths);
        }
        $this->glyphWidths = $glyphWidths;

        /* There is an optional table of left side bearings which is sometimes
         * used for monospaced fonts. We don't use the left side bearing, so
         * we can safely ignore it.
         */
    }


    /**
     * Parses the OpenType cmap (Character to Glyph Mapping) table.
     *
     * The cmap table provides the maps from character codes to font glyphs.
     * There are usually at least two character maps in a font: Microsoft Unicode
     * and Macintosh Roman. For very complex fonts, there may also be mappings
     * for the characters in the Unicode Surrogates Area, which are UCS-4
     * characters.
     *
     * @todo Need to rework the selection logic for picking a subtable. We should
     *   have an explicit list of preferences, followed by a list of those that
     *   are tolerable. Most specifically, since everything above this layer deals
     *   in Unicode, we need to be sure to only accept format 0 MacRoman tables.
     *
     * @throws Zend_Pdf_Exception
     */
    protected function _parseCmapTable()
    {
        $this->_jumpToTable('cmap');
        $baseOffset = $this->_tableDirectory['cmap']['offset'];

        /* We only understand version 0 tables.
         */
        $tableVersion = $this->readUInt(2);
        if ($tableVersion != 0) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Unable to read version $tableVersion table",
                                         Zend_Pdf_Exception::DONT_UNDERSTAND_TABLE_VERSION);
        }
        $this->_debugLog('Version %d table', $tableVersion);

        $subtableCount = $this->readUInt(2);
        $this->_debugLog('%d subtables', $subtableCount);

        /* Like the name table, there may be many different encoding subtables
         * present. Ideally, we are looking for an acceptable Unicode table.
         */
        $subtables = array();
        for ($subtableIndex = 0; $subtableIndex < $subtableCount; $subtableIndex++) {

            $platformID = $this->readUInt(2);
            $encodingID = $this->readUInt(2);

            if (! ( (($platformID == 0) && ($encodingID == 3)) ||    // Unicode 2.0 or later
                    (($platformID == 0) && ($encodingID == 0)) ||    // Unicode
                    (($platformID == 3) && ($encodingID == 1)) ||    // Microsoft Unicode
                    (($platformID == 1) && ($encodingID == 0))       // Mac Roman
                   ) ) {
                $this->_debugLog('Unsupported encoding: platformID: %d; encodingID: %d; skipping',
                                 $platformID, $encodingID);
                $this->skipBytes(4);
                continue;
            }

            $subtableOffset = $this->readUInt(4);
            if ($subtableOffset < 0) {    // Sanity check for 4-byte unsigned on 32-bit platform
                $this->_debugLog('Offset 0x%x out of range for platformID: %d; skipping',
                                 $subtableOffset, $platformID);
                continue;
            }

            $this->_debugLog('Found subtable; platformID: %d; encodingID: %d; offset: 0x%x (0x%x)',
                             $platformID, $encodingID, $baseOffset + $subtableOffset, $subtableOffset);

            $subtables[$platformID][$encodingID][] = $subtableOffset;
        }

        /* In preferred order, find a subtable to use.
         */
        $offsets = array();

        /* Unicode 2.0 or later semantics
         */
        if (isset($subtables[0][3])) {
            foreach ($subtables[0][3] as $offset) {
                $offsets[] = $offset;
            }
        }

        /* Unicode default semantics
         */
        if (isset($subtables[0][0])) {
            foreach ($subtables[0][0] as $offset) {
                $offsets[] = $offset;
            }
        }

        /* Microsoft Unicode
         */
        if (isset($subtables[3][1])) {
            foreach ($subtables[3][1] as $offset) {
                $offsets[] = $offset;
            }
        }

        /* Mac Roman.
         */
        if (isset($subtables[1][0])) {
            foreach ($subtables[1][0] as $offset) {
                $offsets[] = $offset;
            }
        }

        $cmapType = -1;

        foreach ($offsets as $offset) {
            $cmapOffset = $baseOffset + $offset;
            $this->moveToOffset($cmapOffset);
            $format = $this->readUInt(2);
            $language = -1;
            switch ($format) {
                case 0x0:
                    $cmapLength = $this->readUInt(2);
                    $language = $this->readUInt(2);
                    if ($language != 0) {
                        $this->_debugLog('Type 0 cmap tables must be language-independent;'
                                         . ' language: %d; skipping', $language);
                        continue;
                    }
                    break;

                case 0x4:    // break intentionally omitted
                case 0x6:
                    $cmapLength = $this->readUInt(2);
                    $language = $this->readUInt(2);
                    if ($language != 0) {
                        $this->_debugLog('Warning: cmap tables must be language-independent - this font'
                                         . ' may not work properly; language: %d', $language);
                    }
                    break;

                case 0x2:    // break intentionally omitted
                case 0x8:    // break intentionally omitted
                case 0xa:    // break intentionally omitted
                case 0xc:
                    $this->_debugLog('Format: 0x%x currently unsupported; skipping', $format);
                    continue;
                    //$this->skipBytes(2);
                    //$cmapLength = $this->readUInt(4);
                    //$language = $this->readUInt(4);
                    //if ($language != 0) {
                    //    $this->_debugLog('Warning: cmap tables must be language-independent - this font'
                    //                     . ' may not work properly; language: %d', $language);
                    //}
                    //break;

                default:
                    $this->_debugLog('Unknown subtable format: 0x%x; skipping', $format);
                    continue;
            }
            $cmapType = $format;
            break;
        }
        if ($cmapType == -1) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Unable to find usable cmap table',
                                         Zend_Pdf_Exception::CANT_FIND_GOOD_CMAP);
        }

        /* Now extract the subtable data and create a Zend_Pdf_FontCmap object.
         */
        $this->_debugLog('Using cmap type %d; offset: 0x%x; length: %d',
                         $cmapType, $cmapOffset, $cmapLength);
        $this->moveToOffset($cmapOffset);
        $cmapData = $this->readBytes($cmapLength);

        require_once 'Zend/Pdf/Cmap.php';
        $this->cmap = Zend_Pdf_Cmap::cmapWithTypeData($cmapType, $cmapData);
    }


    /**
     * Reads the scaler type from the header of the OpenType font file and
     * returns it as an unsigned long integer.
     *
     * The scaler type defines the type of font: OpenType font files may contain
     * TrueType or PostScript outlines. Throws an exception if the scaler type
     * is not recognized.
     *
     * @return integer
     * @throws Zend_Pdf_Exception
     */
    protected function _readScalerType()
    {
        if ($this->_scalerType != 0) {
            return $this->_scalerType;
        }

        $this->moveToOffset(0);

        $this->_scalerType = $this->readUInt(4);

        switch ($this->_scalerType) {
            case 0x00010000:    // version 1.0 - Windows TrueType signature
                $this->_debugLog('Windows TrueType signature');
                break;

            case 0x74727565:    // 'true' - Macintosh TrueType signature
                $this->_debugLog('Macintosh TrueType signature');
                break;

            case 0x4f54544f:    // 'OTTO' - the CFF signature
                $this->_debugLog('PostScript CFF signature');
                break;

            case 0x74797031:    // 'typ1'
                require_once 'Zend/Pdf/Exception.php';
                throw new Zend_Pdf_Exception('Unsupported font type: PostScript in sfnt wrapper',
                                             Zend_Pdf_Exception::WRONG_FONT_TYPE);

            default:
                require_once 'Zend/Pdf/Exception.php';
                throw new Zend_Pdf_Exception('Not an OpenType font file',
                                             Zend_Pdf_Exception::WRONG_FONT_TYPE);
        }
        return $this->_scalerType;
    }

    /**
     * Validates a given table's existence, then sets the file pointer to the
     * start of that table.
     *
     * @param string $tableName
     * @throws Zend_Pdf_Exception
     */
    protected function _jumpToTable($tableName)
    {
        if (empty($this->_tableDirectory[$tableName])) {    // do not allow NULL or zero
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Required table '$tableName' not found!",
                                         Zend_Pdf_Exception::REQUIRED_TABLE_NOT_FOUND);
        }
        $this->_debugLog("Parsing $tableName table...");
        $this->moveToOffset($this->_tableDirectory[$tableName]['offset']);
    }

    /**
     * Reads the fixed 16.16 table version number and checks for compatibility.
     * If the version is incompatible, throws an exception. If it is compatible,
     * returns the version number.
     *
     * @param float $minVersion Minimum compatible version number.
     * @param float $maxVertion Maximum compatible version number.
     * @return float Table version number.
     * @throws Zend_Pdf_Exception
     */
    protected function _readTableVersion($minVersion, $maxVersion)
    {
        $tableVersion = $this->readFixed(16, 16);
        if (($tableVersion < $minVersion) || ($tableVersion > $maxVersion)) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception("Unable to read version $tableVersion table",
                                         Zend_Pdf_Exception::DONT_UNDERSTAND_TABLE_VERSION);
        }
        $this->_debugLog('Version %.2f table', $tableVersion);
        return $tableVersion;
    }

    /**
     * Utility method that returns ISO 639 two-letter language codes from the
     * TrueType platform and language ID. Returns NULL for languages that are
     * not supported.
     *
     * @param integer $platformID
     * @param integer $encodingID
     * @return string | null
     */
    protected function _languageCodeForPlatform($platformID, $languageID)
    {
        if ($platformID == 3) {    // Microsoft encoding.
            /* The low-order bytes specify the language, the high-order bytes
             * specify the dialect. We just care about the language. For the
             * complete list, see:
             *   http://www.microsoft.com/globaldev/reference/lcid-all.mspx
             */
            $languageID &= 0xff;
            switch ($languageID) {
                case 0x09:
                    return 'en';
                case 0x0c:
                    return 'fr';
                case 0x07:
                    return 'de';
                case 0x10:
                    return 'it';
                case 0x13:
                    return 'nl';
                case 0x1d:
                    return 'sv';
                case 0x0a:
                    return 'es';
                case 0x06:
                    return 'da';
                case 0x16:
                    return 'pt';
                case 0x14:
                    return 'no';
                case 0x0d:
                    return 'he';
                case 0x11:
                    return 'ja';
                case 0x01:
                    return 'ar';
                case 0x0b:
                    return 'fi';
                case 0x08:
                    return 'el';

                default:
                    return null;
            }

        } else if ($platformID == 1) {    // Macintosh encoding.
            switch ($languageID) {
                case 0:
                    return 'en';
                case 1:
                    return 'fr';
                case 2:
                    return 'de';
                case 3:
                    return 'it';
                case 4:
                    return 'nl';
                case 5:
                    return 'sv';
                case 6:
                    return 'es';
                case 7:
                    return 'da';
                case 8:
                    return 'pt';
                case 9:
                    return 'no';
                case 10:
                    return 'he';
                case 11:
                    return 'ja';
                case 12:
                    return 'ar';
                case 13:
                    return 'fi';
                case 14:
                    return 'el';

                default:
                    return null;
            }

        } else {    // Unknown encoding.
            return null;
        }
    }

}