<?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-2012 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: OpenType.php 24594 2012-01-05 21:27:01Z matthew $
*/
/** 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-2012 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;
} else if ($this->isBitSet(1, $embeddingFlags)) {
/* Restricted license embedding. 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;
}
}
}