. */ include_once('TTF.php'); class TTFsubset { const VERBOSE = false; // For debugging private $TTFchars; function doSubset($fontFile, $chars) { $ttf = new TTF(file_get_contents($fontFile)); $head = $ttf->unmarshalHead(); $indexToLocFormat = $head['indexToLocFormat']; $hhea = $ttf->unmarshalHhea(); $numberOfHMetrics = $hhea['numberOfHMetrics']; $maxp = $ttf->unmarshalMAXP(); $numGlyphs = $maxp['numGlyphs']; $orgCvt_Raw = $ttf->getTableRaw('cvt '); $orgPrepRaw = $ttf->getTableRaw('prep'); $orgFpgmRaw = $ttf->getTableRaw('fpgm'); $hmtx = $ttf->unmarshalHmtx($numberOfHMetrics, $numGlyphs); $loca = $ttf->unmarshalLoca($indexToLocFormat, $numGlyphs); $glyf = $ttf->unmarshalGlyf($loca); $cmap = $ttf->unmarshalCmap(); //////////////////////////////////////////////////////////////////////////////// $this->collectChars($chars, $cmap, $loca, $glyf); // Construct new hmtx $allMetrics = array(); foreach ($this->TTFchars as $TTFchar) { $allMetrics[] = TTF::getHMetrics($hmtx, $numberOfHMetrics, $TTFchar->orgIndex); } // Split to metrics and lsbs $numAllMetrics = count($allMetrics); $lastMetric = $allMetrics[$numAllMetrics - 1]; // Looping from last to first, collect a sequence of metrics that have same advance width as last for ($i = $numAllMetrics - 1; $i > 0; $i--) { $metric = $allMetrics[$i - 1]; if ($metric[0] != $lastMetric[0]) { break; } } if ($i == 0) { // All metrics have same advance width $newNumberOfHMetrics = 1; } else if ($i == $numAllMetrics - 1) { $newNumberOfHMetrics = $numAllMetrics; } else { $newNumberOfHMetrics = $i + 1; } $metrics = array(); $lsbs = array(); for ($i = 0; $i < $numAllMetrics; $i++) { if ($i < $newNumberOfHMetrics) { $metrics[] = $allMetrics[$i]; } else { $lsbs[] = $allMetrics[$i][1]; } } $newHmtx = array('metrics' => $metrics, 'lsbs' => $lsbs); // Construct new cmap $newTables = array(); foreach ($cmap['tables'] as $table) { $platformID = $table['platformID']; $platformSpecificID = $table['platformSpecificID']; $format = $table['format']; $length = $table['length']; $version = $table['version']; if ($format == 0) { $glyphIdArray = $table['glyphIdArray']; for ($i = 0; $i < count($glyphIdArray); $i++) { $glyphIdArray[$i] = $this->map($glyphIdArray[$i]); } $newTables[] = array('platformID' => $platformID, 'platformSpecificID' => $platformSpecificID, 'format' => $format, 'length' => 0, // To be calculated 'version' => $version, 'glyphIdArray' => $glyphIdArray); } else if ($format == 4) { $newEndCountArray = array(); $newStartCountArray = array(); $newIdDeltaArray = array(); $newIdRangeOffsetArray = array(); $newGlyphIdArray = array(); // Skip entries with null charCode $i = 0; $cnt = count($this->TTFchars); while ($i < $cnt) { if ($this->TTFchars[$i]->charCode !== null) { break; } $i++; } while ($i < $cnt) { //XXX something better here // Collect a sequence with increasing charCode and newIndex $j = $i; while ($i < $cnt) { if ($this->TTFchars[$i]->charCode - $this->TTFchars[$j]->charCode != $i - $j || $this->TTFchars[$i]->newIndex - $this->TTFchars[$j]->newIndex != $i - $j) { break; } $i++; } $newEndCountArray[] = $this->TTFchars[$i - 1]->charCode; $newStartCountArray[] = $this->TTFchars[$j]->charCode; $newIdDeltaArray[] = 65536 + $this->TTFchars[$j]->newIndex - $this->TTFchars[$j]->charCode; $newIdRangeOffsetArray[] = 0; } $newEndCountArray[] = 65535; $newStartCountArray[] = 65535; $newIdDeltaArray[] = 1; $newIdRangeOffsetArray[] = 0; $newSegCount = count($newEndCountArray); if (self::VERBOSE) { echo "ARRAYS\n"; for ($i = 0; $i < $newSegCount; $i++) { echo sprintf("%5d %5d %5d %5d\n", $newEndCountArray[$i], $newStartCountArray[$i], $newIdDeltaArray[$i], $newIdRangeOffsetArray[$i]); } } $newTables[] = array('platformID' => $platformID, 'platformSpecificID' => $platformSpecificID, 'format' => $format, 'length' => 0, // To be calculated 'version' => $version, 'segCount' => $newSegCount, 'endCountArray' => $newEndCountArray, 'startCountArray' => $newStartCountArray, 'idDeltaArray' => $newIdDeltaArray, 'idRangeOffsetArray' => $newIdRangeOffsetArray, 'glyphIdArray' => $newGlyphIdArray); } else if ($format == 6) { $glyphIdArray = $table['glyphIdArray']; for ($i = 0; $i < count($glyphIdArray); $i++) { $glyphIdArray[$i] = $this->map($glyphIdArray[$i]); } $newTables[] = array('platformID' => $platformID, 'platformSpecificID' => $platformSpecificID, 'format' => $format, 'length' => 0, 'version' => $version, 'firstCode' => $firstCode, 'entryCount' => $entryCount, 'glyphIdArray' => $glyphIdArray); } else { throw new Exception('Internal error'); } } $newCmap = array('version' => $cmap['version'], 'numTables' => $cmap['numTables'], 'tables' => $newTables); // Construct new loca and glyf $newGlyf = array(); $newLoca = array(); $offset = 0; foreach ($this->TTFchars as $TTFchar) { $description = $TTFchar->description; $len = strlen($description); if (($len % 4) != 0) { $toPad = 4 - ($len % 4); $description .= str_repeat(chr(0), $toPad); $len += $toPad; } $newGlyf[] = $description; $newLoca[] = $offset; $offset += $len; } $newLoca[] = $offset; $newIndexToLocFormat = $offset <= 0x20000 ? 0 : 1; $newNumGlyphs = count($this->TTFchars); $head['indexToLocFormat'] = $newIndexToLocFormat; $hhea['numberOfHMetrics'] = $newNumberOfHMetrics; $maxp['numGlyphs'] = $newNumGlyphs; //////////////////////////////////////////////////////////////////////////////// $newHeadRaw = TTF::marshalHead($head); $newHheaRaw = TTF::marshalHhea($hhea); $newMaxpRaw = TTF::marshalMAXP($maxp); $newHmtxRaw = TTF::marshalHmtx($newHmtx['metrics'], $newHmtx['lsbs']); $newCmapRaw = TTF::marshalCmap($newCmap); $newLocaRaw = TTF::marshalLoca($newLoca, $newIndexToLocFormat, $newNumGlyphs); $newGlyfRaw = TTF::marshalGlyf($newGlyf); $tables = array(); $tables['head'] = $newHeadRaw; $tables['hhea'] = $newHheaRaw; $tables['maxp'] = $newMaxpRaw; $tables['loca'] = $newLocaRaw; $tables['cvt '] = $orgCvt_Raw; $tables['prep'] = $orgPrepRaw; $tables['glyf'] = $newGlyfRaw; $tables['hmtx'] = $newHmtxRaw; $tables['fpgm'] = $orgFpgmRaw; $tables['cmap'] = $newCmapRaw; $out = TTF::marshalAll($tables); return $out; } private function collectChars($chars, $cmap, $loca, $glyf) { $this->TTFchars = array(); // Push index 0 (missing character) anyhow $this->TTFchars[] = new TTFchar(null, 0, 0, $glyf[0]); if (($unicodeEncodingTable = TTF::getEncodingTable($cmap, 3, 1)) === null) { throw new Exception('No unicode (3,1) encoding table found'); } for ($i = 0; $i < strlen($chars); $i += 2) { $charCode = self::ORD(substr($chars, $i, 2)); $orgIndex = TTF::characterToIndex($unicodeEncodingTable, $charCode); $description = $glyf[$orgIndex]; if (!$this->orgIndexAlreadyExists($orgIndex)) { $this->TTFchars[] = new TTFchar($charCode, $orgIndex, 0, $description); } } // If there exist composite glyphs (numberOfContours < 0), we have to append the components // WARNING: This loop appends to $this->TTFchars (foreach will not work) for ($i = 0; $i < count($this->TTFchars); $i++) { $TTFchar = $this->TTFchars[$i]; $description = $TTFchar->description; if (strlen($description) == 0) { continue; } $glyph = TTF::getGlyph($description); if ($glyph['numberOfContours'] >= 0) { continue; } foreach ($glyph['components'] as $component) { if (!$this->orgIndexAlreadyExists($component['glyphIndex'])) { $orgIndex2 = $component['glyphIndex']; $description2 = $glyf[$orgIndex2]; $this->TTFchars[] = new TTFchar(null, $orgIndex2, 0, $description2); } } } usort($this->TTFchars, array('TTFsubset', 'TTFcharComparatorOnCharCode')); // Assign newIndex $newIndex = 0; for ($i = 0; $i < count($this->TTFchars); $i++) { $this->TTFchars[$i]->newIndex = $newIndex++; } // If there exist composite glyphs, replace the components' glyphIndices // First construct a from=>to array $replacements = array(); foreach ($this->TTFchars as $TTFchar) { $orgIndex = $TTFchar->orgIndex; $newIndex = $TTFchar->newIndex; $replacements[$orgIndex] = $newIndex; } for ($i = 0; $i < count($this->TTFchars); $i++) { $TTFchar = $this->TTFchars[$i]; $description = $TTFchar->description; if (strlen($description) == 0) { continue; } $glyph = TTF::getGlyph($description); if ($glyph['numberOfContours'] >= 0) { continue; } $newDescription = TTF::replaceComponentsOfCompositeGlyph($description, $replacements); $this->TTFchars[$i]->description = $newDescription; } if (self::VERBOSE) { foreach ($this->TTFchars as $TTFchar) { echo sprintf("%4d %4d %4d %4d\n", $TTFchar->charCode, $TTFchar->orgIndex, $TTFchar->newIndex, strlen($TTFchar->description)); } echo sprintf("TTFchars size is %d\n", count($this->TTFchars)); } } private function orgIndexAlreadyExists($orgIndex) { foreach ($this->TTFchars as $TTFchar) { if ($TTFchar->orgIndex == $orgIndex) { return true; } } return false; } private function TTFcharComparatorOnCharCode($t1, $t2) { return $t1->charCode - $t2->charCode; } private function map($index) { for ($i = 0; $i < count($this->TTFchars); $i++) { $TTFchar = $this->TTFchars[$i]; if ($TTFchar->orgIndex == $index) { return $TTFchar->newIndex; } } return 0; // Map to index 0 } private static function ORD($str) { $val = 0; for ($i = 0; $i < strlen($str); $i++) { $val = 256 * $val + ord($str{$i}); } return $val; } } class TTFchar { public $charCode; public $orgIndex; public $newIndex; public $description; function __construct($charCode, $orgIndex, $newIndex, $description) { $this->charCode = $charCode; $this->orgIndex = $orgIndex; $this->newIndex = $newIndex; $this->description = $description; } } ?>