*
  • {@link http://developer.apple.com/textfonts/TTRefMan/} *
  • {@link http://www.microsoft.com/typography/OTSPEC/} *
  • {@link http://partners.adobe.com/public/developer/opentype/index_spec.html} * * * @package Zend_Pdf * @subpackage FileParser * @copyright Copyright (c) 2005-2015 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; } } }