composer package updates
[openemr.git] / vendor / mpdf / mpdf / src / TTFontFile.php
blob00476463ac6661e291ceedc32b8d3dbac2854e5c
1 <?php
3 namespace Mpdf;
5 use Mpdf\Fonts\FontCache;
6 use Mpdf\Fonts\GlyphOperator;
8 // NOTE*** If you change the defined constants below, be sure to delete all temporary font data files in /ttfontdata/
9 // to force mPDF to regenerate cached font files.
10 if (!defined('_OTL_OLD_SPEC_COMPAT_2')) {
11 define("_OTL_OLD_SPEC_COMPAT_2", true);
14 // Define the value used in the "head" table of a created TTF file
15 // 0x74727565 "true" for Mac
16 // 0x00010000 for Windows
17 // Either seems to work for a font embedded in a PDF file
18 // when read by Adobe Reader on a Windows PC(!)
19 if (!defined('_TTF_MAC_HEADER')) {
20 define("_TTF_MAC_HEADER", false);
23 // Recalculate correct metadata/profiles when making subset fonts (not SIP/SMP)
24 // e.g. xMin, xMax, maxNContours
25 if (!defined('_RECALC_PROFILE')) {
26 define("_RECALC_PROFILE", false);
29 // mPDF 5.7.1
30 if (!function_exists('\Mpdf\unicode_hex')) {
31 function unicode_hex($unicode_dec)
33 return (sprintf("%05s", strtoupper(dechex($unicode_dec))));
37 /**
38 * TTFontFile class
40 * This class is based on The ReportLab Open Source PDF library
41 * written in Python - http://www.reportlab.com/software/opensource/
42 * together with ideas from the OpenOffice source code and others.
43 * This header must be retained in any redistribution or
44 * modification of the file.
46 * @author Ian Back <ianb@bpm1.com>
47 * @license LGPL
49 class TTFontFile
52 private $fontCache;
54 private $fontDescriptor;
56 var $GPOSFeatures; // mPDF 5.7.1
58 var $GPOSLookups; // mPDF 5.7.1
60 var $GPOSScriptLang; // mPDF 5.7.1
62 var $MarkAttachmentType; // mPDF 5.7.1
64 var $MarkGlyphSets; // mPDF 7.5.1
66 var $GlyphClassMarks; // mPDF 5.7.1
68 var $GlyphClassLigatures; // mPDF 5.7.1
70 var $GlyphClassBases; // mPDF 5.7.1
72 var $GlyphClassComponents; // mPDF 5.7.1
74 var $GSUBScriptLang; // mPDF 5.7.1
76 var $rtlPUAstr; // mPDF 5.7.1
78 //var $rtlPUAarr; // mPDF 5.7.1
79 var $fontkey; // mPDF 5.7.1
81 var $useOTL; // mPDF 5.7.1 var $panose;
83 var $maxUni;
85 var $sFamilyClass;
87 var $sFamilySubClass;
89 var $sipset;
91 var $smpset;
93 var $_pos;
95 var $numTables;
97 var $searchRange;
99 var $entrySelector;
101 var $rangeShift;
103 var $tables;
105 var $otables;
107 var $filename;
109 var $fh;
111 var $glyphPos;
113 var $charToGlyph;
115 var $ascent;
117 var $descent;
119 var $lineGap; // mPDF 6
121 var $hheaascent;
123 var $hheadescent;
125 var $hhealineGap; // mPDF 6
127 var $advanceWidthMax; // mPDF 6
129 var $typoAscender; // mPDF 6
131 var $typoDescender; // mPDF 6
133 var $typoLineGap; // mPDF 6
135 var $usWinAscent; // mPDF 6
137 var $usWinDescent; // mPDF 6
139 var $strikeoutSize;
141 var $strikeoutPosition;
143 var $name;
145 var $familyName;
147 var $styleName;
149 var $fullName;
151 var $uniqueFontID;
153 var $unitsPerEm;
155 var $bbox;
157 var $capHeight;
159 var $xHeight; // mPDF 6
161 var $stemV;
163 var $italicAngle;
165 var $flags;
167 var $underlinePosition;
169 var $underlineThickness;
171 var $charWidths;
173 var $defaultWidth;
175 var $maxStrLenRead;
177 var $numTTCFonts;
179 var $TTCFonts;
181 var $maxUniChar;
183 var $kerninfo;
185 var $haskernGPOS;
187 var $hassmallcapsGSUB;
189 var $codeToGlyph;
191 var $glyphdata;
193 var $LuCoverage;
195 public $panose;
197 public $version;
199 public $fontRevision;
201 public $restrictedUse;
203 public $glyphIDtoUni;
205 public $glyphToChar;
207 public $GSUBFeatures;
209 public $GSUBLookups;
211 public $GSLuCoverage;
213 public function __construct(FontCache $fontCache, $fontDescriptor)
215 $this->fontCache = $fontCache;
216 $this->fontDescriptor = $fontDescriptor;
218 // Maximum size of glyf table to read in as string (otherwise reads each glyph from file)
219 $this->maxStrLenRead = 200000;
222 function getMetrics($file, $fontkey, $TTCfontID = 0, $debug = false, $BMPonly = false, $useOTL = 0)
224 $this->useOTL = $useOTL; // mPDF 5.7.1
225 $this->fontkey = $fontkey; // mPDF 5.7.1
226 $this->filename = $file;
227 $this->fh = fopen($file, 'rb');
229 if (!$this->fh) {
230 throw new \Mpdf\MpdfException('Can\'t open file ' . $file);
233 $this->_pos = 0;
234 $this->charWidths = '';
235 $this->glyphPos = [];
236 $this->charToGlyph = [];
237 $this->tables = [];
238 $this->otables = [];
239 $this->kerninfo = [];
240 $this->haskernGPOS = [];
241 $this->hassmallcapsGSUB = [];
242 $this->ascent = 0;
243 $this->descent = 0;
244 $this->lineGap = 0; // mPDF 6
245 $this->hheaascent = 0; // mPDF 6
246 $this->hheadescent = 0; // mPDF 6
247 $this->hhealineGap = 0; // mPDF 6
248 $this->xHeight = 0; // mPDF 6
249 $this->capHeight = 0; // mPDF 6
250 $this->panose = [];
251 $this->sFamilyClass = 0;
252 $this->sFamilySubClass = 0;
253 $this->typoAscender = 0; // mPDF 6
254 $this->typoDescender = 0; // mPDF 6
255 $this->typoLineGap = 0; // mPDF 6
256 $this->usWinAscent = 0; // mPDF 6
257 $this->usWinDescent = 0; // mPDF 6
258 $this->advanceWidthMax = 0; // mPDF 6
259 $this->strikeoutSize = 0;
260 $this->strikeoutPosition = 0;
261 $this->numTTCFonts = 0;
262 $this->TTCFonts = [];
263 $this->version = $version = $this->read_ulong();
264 $this->panose = [];
266 if ($version == 0x4F54544F) {
267 throw new \Mpdf\MpdfException("Postscript outlines are not supported");
270 if ($version == 0x74746366 && !$TTCfontID) {
271 throw new \Mpdf\MpdfException("ERROR - You must define the TTCfontID for a TrueType Collection in config_fonts.php (" . $file . ")");
274 if (!in_array($version, [0x00010000, 0x74727565]) && !$TTCfontID) {
275 throw new \Mpdf\MpdfException("Not a TrueType font: version=" . $version);
278 if ($TTCfontID > 0) {
279 $this->version = $version = $this->read_ulong(); // TTC Header version now
280 if (!in_array($version, [0x00010000, 0x00020000])) {
281 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
283 $this->numTTCFonts = $this->read_ulong();
284 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
285 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
287 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
288 $this->version = $version = $this->read_ulong(); // TTFont version again now
291 $this->readTableDirectory($debug);
292 $this->extractInfo($debug, $BMPonly, $useOTL);
293 fclose($this->fh);
296 function readTableDirectory($debug = false)
298 $this->numTables = $this->read_ushort();
299 $this->searchRange = $this->read_ushort();
300 $this->entrySelector = $this->read_ushort();
301 $this->rangeShift = $this->read_ushort();
302 $this->tables = [];
303 for ($i = 0; $i < $this->numTables; $i++) {
304 $record = [];
305 $record['tag'] = $this->read_tag();
306 $record['checksum'] = [$this->read_ushort(), $this->read_ushort()];
307 $record['offset'] = $this->read_ulong();
308 $record['length'] = $this->read_ulong();
309 $this->tables[$record['tag']] = $record;
311 if ($debug) {
312 $this->checksumTables();
316 function checksumTables()
318 // Check the checksums for all tables
319 foreach ($this->tables as $t) {
320 if ($t['length'] > 0 && $t['length'] < $this->maxStrLenRead) { // 1.02
321 $table = $this->get_chunk($t['offset'], $t['length']);
322 $checksum = $this->calcChecksum($table);
323 if ($t['tag'] == 'head') {
324 $up = unpack('n*', substr($table, 8, 4));
325 $adjustment[0] = $up[1];
326 $adjustment[1] = $up[2];
327 $checksum = $this->sub32($checksum, $adjustment);
329 $xchecksum = $t['checksum'];
330 if ($xchecksum != $checksum) {
331 throw new \Mpdf\MpdfException(sprintf('TTF file "%s": invalid checksum %s table: %s (expected %s)', $this->filename, dechex($checksum[0]) . dechex($checksum[1]), $t['tag'], dechex($xchecksum[0]) . dechex($xchecksum[1])));
337 function sub32($x, $y)
339 $xlo = $x[1];
340 $xhi = $x[0];
341 $ylo = $y[1];
342 $yhi = $y[0];
343 if ($ylo > $xlo) {
344 $xlo += 1 << 16;
345 $yhi += 1;
347 $reslo = $xlo - $ylo;
348 if ($yhi > $xhi) {
349 $xhi += 1 << 16;
351 $reshi = $xhi - $yhi;
352 $reshi = $reshi & 0xFFFF;
354 return [$reshi, $reslo];
357 function calcChecksum($data)
359 if (strlen($data) % 4) {
360 $data .= str_repeat("\0", (4 - (strlen($data) % 4)));
362 $len = strlen($data);
363 $hi = 0x0000;
364 $lo = 0x0000;
365 for ($i = 0; $i < $len; $i += 4) {
366 $hi += (ord($data[$i]) << 8) + ord($data[$i + 1]);
367 $lo += (ord($data[$i + 2]) << 8) + ord($data[$i + 3]);
368 $hi += ($lo >> 16) & 0xFFFF;
369 $lo = $lo & 0xFFFF;
371 $hi = $hi & 0xFFFF; // mPDF 5.7.1
372 return [$hi, $lo];
375 function get_table_pos($tag)
377 if (!isset($this->tables[$tag])) {
378 return [0, 0];
380 $offset = $this->tables[$tag]['offset'];
381 $length = $this->tables[$tag]['length'];
383 return [$offset, $length];
386 function seek($pos)
388 $this->_pos = $pos;
389 fseek($this->fh, $this->_pos);
392 function skip($delta)
394 $this->_pos = $this->_pos + $delta;
395 fseek($this->fh, $delta, SEEK_CUR);
398 function seek_table($tag, $offset_in_table = 0)
400 $tpos = $this->get_table_pos($tag);
401 $this->_pos = $tpos[0] + $offset_in_table;
402 fseek($this->fh, $this->_pos);
404 return $this->_pos;
407 function read_tag()
409 $this->_pos += 4;
411 return fread($this->fh, 4);
414 function read_short()
416 $this->_pos += 2;
417 $s = fread($this->fh, 2);
418 $a = (ord($s[0]) << 8) + ord($s[1]);
419 if ($a & (1 << 15)) {
420 $a = ($a - (1 << 16));
423 return $a;
426 function unpack_short($s)
428 $a = (ord($s[0]) << 8) + ord($s[1]);
429 if ($a & (1 << 15)) {
430 $a = ($a - (1 << 16));
433 return $a;
436 function read_ushort()
438 $this->_pos += 2;
439 $s = fread($this->fh, 2);
441 return (ord($s[0]) << 8) + ord($s[1]);
444 function read_ulong()
446 $this->_pos += 4;
447 $s = fread($this->fh, 4);
449 // if large uInt32 as an integer, PHP converts it to -ve
450 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24
453 function get_ushort($pos)
455 fseek($this->fh, $pos);
456 $s = fread($this->fh, 2);
458 return (ord($s[0]) << 8) + ord($s[1]);
461 function get_ulong($pos)
463 fseek($this->fh, $pos);
464 $s = fread($this->fh, 4);
466 // iF large uInt32 as an integer, PHP converts it to -ve
467 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24
470 function pack_short($val)
472 if ($val < 0) {
473 $val = abs($val);
474 $val = ~$val;
475 $val += 1;
478 return pack("n", $val);
481 function splice($stream, $offset, $value)
483 return substr($stream, 0, $offset) . $value . substr($stream, $offset + strlen($value));
486 function _set_ushort($stream, $offset, $value)
488 $up = pack("n", $value);
490 return $this->splice($stream, $offset, $up);
493 function _set_short($stream, $offset, $val)
495 if ($val < 0) {
496 $val = abs($val);
497 $val = ~$val;
498 $val += 1;
500 $up = pack("n", $val);
502 return $this->splice($stream, $offset, $up);
505 function get_chunk($pos, $length)
507 fseek($this->fh, $pos);
508 if ($length < 1) {
509 return '';
512 return (fread($this->fh, $length));
515 function get_table($tag)
517 list($pos, $length) = $this->get_table_pos($tag);
518 if ($length == 0) {
519 return '';
521 fseek($this->fh, $pos);
523 return (fread($this->fh, $length));
526 function add($tag, $data)
528 if ($tag == 'head') {
529 $data = $this->splice($data, 8, "\0\0\0\0");
531 $this->otables[$tag] = $data;
534 /////////////////////////////////////////////////////////////////////////////////////////
535 function getCTG($file, $TTCfontID = 0, $debug = false, $useOTL = false)
537 // mPDF 5.7.1
538 // Only called if font is not to be used as embedded subset i.e. NOT called for SIP/SMP fonts
539 $this->useOTL = $useOTL; // mPDF 5.7.1
540 $this->filename = $file;
541 $this->fh = fopen($file, 'rb');
543 if (!$this->fh) {
544 throw new \Mpdf\MpdfException('Can\'t open file ' . $file);
547 $this->_pos = 0;
548 $this->charWidths = '';
549 $this->glyphPos = [];
550 $this->charToGlyph = [];
551 $this->tables = [];
552 $this->numTTCFonts = 0;
553 $this->TTCFonts = [];
554 $this->skip(4);
555 if ($TTCfontID > 0) {
556 $this->version = $version = $this->read_ulong(); // TTC Header version now
557 if (!in_array($version, [0x00010000, 0x00020000])) {
558 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
560 $this->numTTCFonts = $this->read_ulong();
561 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
562 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
564 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
565 $this->version = $version = $this->read_ulong(); // TTFont version again now
567 $this->readTableDirectory($debug);
569 // cmap - Character to glyph index mapping table
570 $cmap_offset = $this->seek_table("cmap");
571 $this->skip(2);
572 $cmapTableCount = $this->read_ushort();
573 $unicode_cmap_offset = 0;
574 for ($i = 0; $i < $cmapTableCount; $i++) {
575 $platformID = $this->read_ushort();
576 $encodingID = $this->read_ushort();
577 $offset = $this->read_ulong();
578 $save_pos = $this->_pos;
579 if ($platformID == 3 && $encodingID == 1) { // Microsoft, Unicode
580 $format = $this->get_ushort($cmap_offset + $offset);
581 if ($format == 4) {
582 $unicode_cmap_offset = $cmap_offset + $offset;
583 break;
585 } else if ($platformID == 0) { // Unicode -- assume all encodings are compatible
586 $format = $this->get_ushort($cmap_offset + $offset);
587 if ($format == 4) {
588 $unicode_cmap_offset = $cmap_offset + $offset;
589 break;
592 $this->seek($save_pos);
595 $glyphToChar = [];
596 $charToGlyph = [];
597 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
599 ///////////////////////////////////
600 // mPDF 5.7.1
601 // Map Unmapped glyphs - from $numGlyphs
602 if ($useOTL) {
603 $this->seek_table("maxp");
604 $this->skip(4);
605 $numGlyphs = $this->read_ushort();
606 $bctr = 0xE000;
607 for ($gid = 1; $gid < $numGlyphs; $gid++) {
608 if (!isset($glyphToChar[$gid])) {
609 while (isset($charToGlyph[$bctr])) {
610 $bctr++;
611 } // Avoid overwriting a glyph already mapped in PUA
612 if ($bctr > 0xF8FF) {
613 throw new \Mpdf\MpdfException($file . " : WARNING - Font cannot map all included glyphs into Private Use Area U+E000 - U+F8FF; cannot use useOTL on this font");
615 $glyphToChar[$gid][] = $bctr;
616 $charToGlyph[$bctr] = $gid;
617 $bctr++;
621 ///////////////////////////////////
623 fclose($this->fh);
625 return ($charToGlyph);
628 /////////////////////////////////////////////////////////////////////////////////////////
629 function getTTCFonts($file)
631 $this->filename = $file;
632 $this->fh = fopen($file, 'rb');
633 if (!$this->fh) {
634 throw new \Mpdf\MpdfException('ERROR - Can\'t open file ' . $file);
636 $this->numTTCFonts = 0;
637 $this->TTCFonts = [];
638 $this->version = $version = $this->read_ulong();
639 if ($version == 0x74746366) {
640 $this->version = $version = $this->read_ulong(); // TTC Header version now
641 if (!in_array($version, [0x00010000, 0x00020000])) {
642 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
644 } else {
645 throw new \Mpdf\MpdfException("ERROR - Not a TrueType Collection: version=" . $version . " - " . $file);
647 $this->numTTCFonts = $this->read_ulong();
648 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
649 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
653 /////////////////////////////////////////////////////////////////////////////////////////
654 /////////////////////////////////////////////////////////////////////////////////////////
656 function extractInfo($debug = false, $BMPonly = false, $useOTL = 0)
658 // Values are all set to 0 or blank at start of getMetrics
659 ///////////////////////////////////
660 // name - Naming table
661 ///////////////////////////////////
662 $name_offset = $this->seek_table("name");
663 $format = $this->read_ushort();
664 if ($format != 0 && $format != 1) {
665 throw new \Mpdf\MpdfException("Unknown name table format " . $format);
667 $numRecords = $this->read_ushort();
668 $string_data_offset = $name_offset + $this->read_ushort();
669 $names = [1 => '', 2 => '', 3 => '', 4 => '', 6 => ''];
670 $K = array_keys($names);
671 $nameCount = count($names);
672 for ($i = 0; $i < $numRecords; $i++) {
673 $platformId = $this->read_ushort();
674 $encodingId = $this->read_ushort();
675 $languageId = $this->read_ushort();
676 $nameId = $this->read_ushort();
677 $length = $this->read_ushort();
678 $offset = $this->read_ushort();
679 if (!in_array($nameId, $K)) {
680 continue;
682 $N = '';
683 if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name
684 $opos = $this->_pos;
685 $this->seek($string_data_offset + $offset);
686 if ($length % 2 != 0) {
687 throw new \Mpdf\MpdfException("PostScript name is UTF-16BE string of odd length");
689 $length /= 2;
690 $N = '';
691 while ($length > 0) {
692 $char = $this->read_ushort();
693 $N .= (chr($char));
694 $length -= 1;
696 $this->_pos = $opos;
697 $this->seek($opos);
698 } else if ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name
699 $opos = $this->_pos;
700 $N = $this->get_chunk($string_data_offset + $offset, $length);
701 $this->_pos = $opos;
702 $this->seek($opos);
704 if ($N && $names[$nameId] == '') {
705 $names[$nameId] = $N;
706 $nameCount -= 1;
707 if ($nameCount == 0) {
708 break;
712 if ($names[6]) {
713 $psName = $names[6];
714 } else if ($names[4]) {
715 $psName = preg_replace('/ /', '-', $names[4]);
716 } else if ($names[1]) {
717 $psName = preg_replace('/ /', '-', $names[1]);
718 } else {
719 $psName = '';
721 if (!$psName) {
722 throw new \Mpdf\MpdfException("Could not find PostScript font name: " . $this->filename);
724 // CHECK IF psName valid (PadaukBook contains illegal characters in Name ID 6 i.e. Postscript Name)
725 $psNameInvalid = false;
726 for ($i = 0; $i < strlen($psName); $i++) {
727 $c = $psName[$i];
728 $oc = ord($c);
729 if ($oc > 126 || strpos(' [](){}<>/%', $c) !== false) {
730 //throw new \Mpdf\MpdfException("psName=".$psName." contains invalid character ".$c." ie U+".ord(c));
731 $psNameInvalid = true;
732 break;
736 if ($psNameInvalid && $names[4]) {
737 $psName = preg_replace('/ /', '-', $names[4]);
740 $this->name = $psName;
741 if ($names[1]) {
742 $this->familyName = $names[1];
743 } else {
744 $this->familyName = $psName;
746 if ($names[2]) {
747 $this->styleName = $names[2];
748 } else {
749 $this->styleName = 'Regular';
751 if ($names[4]) {
752 $this->fullName = $names[4];
753 } else {
754 $this->fullName = $psName;
756 if ($names[3]) {
757 $this->uniqueFontID = $names[3];
758 } else {
759 $this->uniqueFontID = $psName;
762 if (!$psNameInvalid && $names[6]) {
763 $this->fullName = $names[6];
766 ///////////////////////////////////
767 // head - Font header table
768 ///////////////////////////////////
769 $this->seek_table("head");
770 if ($debug) {
771 $ver_maj = $this->read_ushort();
772 $ver_min = $this->read_ushort();
773 if ($ver_maj != 1) {
774 throw new \Mpdf\MpdfException('Unknown head table version ' . $ver_maj . '.' . $ver_min);
776 $this->fontRevision = $this->read_ushort() . $this->read_ushort();
778 $this->skip(4);
779 $magic = $this->read_ulong();
780 if ($magic != 0x5F0F3CF5) {
781 throw new \Mpdf\MpdfException('Invalid head table magic ' . $magic);
783 $this->skip(2);
784 } else {
785 $this->skip(18);
787 $this->unitsPerEm = $unitsPerEm = $this->read_ushort();
788 $scale = 1000 / $unitsPerEm;
789 $this->skip(16);
790 $xMin = $this->read_short();
791 $yMin = $this->read_short();
792 $xMax = $this->read_short();
793 $yMax = $this->read_short();
794 $this->bbox = [($xMin * $scale), ($yMin * $scale), ($xMax * $scale), ($yMax * $scale)];
796 $this->skip(3 * 2);
797 $indexToLocFormat = $this->read_ushort();
798 $glyphDataFormat = $this->read_ushort();
799 if ($glyphDataFormat != 0) {
800 throw new \Mpdf\MpdfException('Unknown glyph data format ' . $glyphDataFormat);
803 ///////////////////////////////////
804 // hhea metrics table
805 ///////////////////////////////////
806 if (isset($this->tables["hhea"])) {
807 $this->seek_table("hhea");
808 $this->skip(4);
809 $hheaAscender = $this->read_short();
810 $hheaDescender = $this->read_short();
811 $hheaLineGap = $this->read_short(); // mPDF 6
812 $hheaAdvanceWidthMax = $this->read_ushort(); // mPDF 6
813 $this->hheaascent = ($hheaAscender * $scale);
814 $this->hheadescent = ($hheaDescender * $scale);
815 $this->hhealineGap = ($hheaLineGap * $scale); // mPDF 6
816 $this->advanceWidthMax = ($hheaAdvanceWidthMax * $scale); // mPDF 6
819 ///////////////////////////////////
820 // OS/2 - OS/2 and Windows metrics table
821 ///////////////////////////////////
822 $use_typo_metrics = false;
823 if (isset($this->tables["OS/2"])) {
824 $this->seek_table("OS/2");
825 $version = $this->read_ushort();
826 $this->skip(2);
827 $usWeightClass = $this->read_ushort();
828 $this->skip(2);
829 $fsType = $this->read_ushort();
830 if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) {
831 $this->restrictedUse = true;
834 // mPDF 6
835 $this->skip(16);
836 $yStrikeoutSize = $this->read_short();
837 $yStrikeoutPosition = $this->read_short();
838 $this->strikeoutSize = ($yStrikeoutSize * $scale);
839 $this->strikeoutPosition = ($yStrikeoutPosition * $scale);
841 $sF = $this->read_short();
842 $this->sFamilyClass = ($sF >> 8);
843 $this->sFamilySubClass = ($sF & 0xFF);
844 $this->_pos += 10; //PANOSE = 10 byte length
845 $panose = fread($this->fh, 10);
846 $this->panose = [];
847 for ($p = 0; $p < strlen($panose); $p++) {
848 $this->panose[] = ord($panose[$p]);
850 //$this->skip(26);
851 // mPDF 6
852 $this->skip(20);
853 $fsSelection = $this->read_ushort();
854 $use_typo_metrics = (($fsSelection & 0x80) == 0x80); // bit#7 = USE_TYPO_METRICS
855 $this->skip(4);
857 $sTypoAscender = $this->read_short();
858 $sTypoDescender = $this->read_short();
859 $sTypoLineGap = $this->read_short(); // mPDF 6
860 if ($sTypoAscender) {
861 $this->typoAscender = ($sTypoAscender * $scale);
862 } // mPDF 6
863 if ($sTypoDescender) {
864 $this->typoDescender = ($sTypoDescender * $scale);
865 } // mPDF 6
866 if ($sTypoLineGap) {
867 $this->typoLineGap = ($sTypoLineGap * $scale);
868 } // mPDF 6
870 $usWinAscent = $this->read_ushort(); // mPDF 6
871 $usWinDescent = $this->read_ushort(); // mPDF 6
872 if ($usWinAscent) {
873 $this->usWinAscent = ($usWinAscent * $scale);
874 } // mPDF 6
875 if ($usWinDescent) {
876 $this->usWinDescent = ($usWinDescent * $scale);
877 } // mPDF 6
879 if ($version > 1) {
880 $this->skip(8); // mPDF 6
881 $sxHeight = $this->read_short();
882 $this->xHeight = ($sxHeight * $scale);
883 $sCapHeight = $this->read_short();
884 $this->capHeight = ($sCapHeight * $scale);
886 } else {
887 $usWeightClass = 400;
889 $this->stemV = 50 + intval(pow(($usWeightClass / 65.0), 2));
891 // FONT DESCRIPTOR METRICS
892 if ($this->fontDescriptor == 'winTypo') {
893 $this->ascent = $this->typoAscender;
894 $this->descent = $this->typoDescender;
895 $this->lineGap = $this->typoLineGap;
896 } else if ($this->fontDescriptor == 'mac') {
897 $this->ascent = $this->hheaascent;
898 $this->descent = $this->hheadescent;
899 $this->lineGap = $this->hhealineGap;
900 } else { // if (_FONT_DESCRIPTOR == 'win') { // default
901 $this->ascent = $this->usWinAscent;
902 $this->descent = -$this->usWinDescent;
903 $this->lineGap = 0;
905 /* Special case - if either the winAscent or winDescent are greater than the
906 font bounding box yMin yMax, then reduce them accordingly.
907 This works with Myanmar Text (Windows 8 version) to give a
908 line-height normal that is equivalent to that produced in browsers.
909 Also Khmer OS = compatible with MSWord, Wordpad and browser. */
910 if ($this->ascent > $this->bbox[3]) {
911 $this->ascent = $this->bbox[3];
913 if ($this->descent < $this->bbox[1]) {
914 $this->descent = $this->bbox[1];
917 /* Override case - if the USE_TYPO_METRICS bit is set on OS/2 fsSelection
918 this is telling the font to use the sTypo values and not the usWinAscent values.
919 This works as a fix with Cambria Math to give a normal line-height;
920 at present, this is the only font I have found with this bit set;
921 although note that MS WordPad and windows FF browser uses the big line-height from winAscent
922 but Word 2007 get it right . */
923 if ($use_typo_metrics && $this->typoAscender) {
924 $this->ascent = $this->typoAscender;
925 $this->descent = $this->typoDescender;
926 $this->lineGap = $this->typoLineGap;
930 ///////////////////////////////////
931 // post - PostScript table
932 ///////////////////////////////////
933 $this->seek_table("post");
934 if ($debug) {
935 $ver_maj = $this->read_ushort();
936 if ($ver_maj < 1 || $ver_maj > 4) {
937 throw new \Mpdf\MpdfException('Unknown post table version ' . $ver_maj);
939 } else {
940 $this->skip(4);
942 $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0;
943 $this->underlinePosition = $this->read_short() * $scale;
944 $this->underlineThickness = $this->read_short() * $scale;
945 $isFixedPitch = $this->read_ulong();
947 $this->flags = 4;
949 if ($this->italicAngle != 0) {
950 $this->flags = $this->flags | 64;
952 if ($usWeightClass >= 600) {
953 $this->flags = $this->flags | 262144;
955 if ($isFixedPitch) {
956 $this->flags = $this->flags | 1;
959 ///////////////////////////////////
960 // hhea - Horizontal header table
961 ///////////////////////////////////
962 $this->seek_table("hhea");
963 if ($debug) {
964 $ver_maj = $this->read_ushort();
965 if ($ver_maj != 1) {
966 throw new \Mpdf\MpdfException('Unknown hhea table version ' . $ver_maj);
968 $this->skip(28);
969 } else {
970 $this->skip(32);
973 $metricDataFormat = $this->read_ushort();
975 if ($metricDataFormat != 0) {
976 throw new \Mpdf\MpdfException('Unknown horizontal metric data format ' . $metricDataFormat);
979 $numberOfHMetrics = $this->read_ushort();
981 if ($numberOfHMetrics == 0) {
982 throw new \Mpdf\MpdfException('Number of horizontal metrics is 0');
985 ///////////////////////////////////
986 // maxp - Maximum profile table
987 ///////////////////////////////////
988 $this->seek_table("maxp");
989 if ($debug) {
990 $ver_maj = $this->read_ushort();
991 if ($ver_maj != 1) {
992 throw new \Mpdf\MpdfException('Unknown maxp table version ' . $ver_maj);
994 } else {
995 $this->skip(4);
997 $numGlyphs = $this->read_ushort();
999 ///////////////////////////////////
1000 // cmap - Character to glyph index mapping table
1001 ///////////////////////////////////
1002 $cmap_offset = $this->seek_table("cmap");
1003 $this->skip(2);
1004 $cmapTableCount = $this->read_ushort();
1005 $unicode_cmap_offset = 0;
1006 for ($i = 0; $i < $cmapTableCount; $i++) {
1007 $platformID = $this->read_ushort();
1008 $encodingID = $this->read_ushort();
1009 $offset = $this->read_ulong();
1010 $save_pos = $this->_pos;
1011 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode
1012 $format = $this->get_ushort($cmap_offset + $offset);
1013 if ($format == 4) {
1014 if (!$unicode_cmap_offset) {
1015 $unicode_cmap_offset = $cmap_offset + $offset;
1017 if ($BMPonly) {
1018 break;
1021 } // Microsoft, Unicode Format 12 table HKCS
1022 else if ((($platformID == 3 && $encodingID == 10) || $platformID == 0) && !$BMPonly) {
1023 $format = $this->get_ushort($cmap_offset + $offset);
1024 if ($format == 12) {
1025 $unicode_cmap_offset = $cmap_offset + $offset;
1026 break;
1029 $this->seek($save_pos);
1032 if (!$unicode_cmap_offset) {
1033 throw new \Mpdf\MpdfException('Font (' . $this->filename . ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)');
1036 $sipset = false;
1037 $smpset = false;
1039 // mPDF 5.7.1
1040 $this->rtlPUAstr = '';
1041 //$this->rtlPUAarr = array();
1042 $this->GSUBScriptLang = [];
1043 $this->GSUBFeatures = [];
1044 $this->GSUBLookups = [];
1045 $this->GPOSScriptLang = [];
1046 $this->GPOSFeatures = [];
1047 $this->GPOSLookups = [];
1048 $this->glyphIDtoUni = '';
1050 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above
1051 if ($format == 12 && !$BMPonly) {
1052 $this->maxUniChar = 0;
1053 $this->seek($unicode_cmap_offset + 4);
1054 $length = $this->read_ulong();
1055 $limit = $unicode_cmap_offset + $length;
1056 $this->skip(4);
1058 $nGroups = $this->read_ulong();
1060 $glyphToChar = [];
1061 $charToGlyph = [];
1062 for ($i = 0; $i < $nGroups; $i++) {
1063 $startCharCode = $this->read_ulong();
1064 $endCharCode = $this->read_ulong();
1065 $startGlyphCode = $this->read_ulong();
1066 // ZZZ98
1067 if ($endCharCode > 0x20000 && $endCharCode < 0x2FFFF) {
1068 $sipset = true;
1069 } else if ($endCharCode > 0x10000 && $endCharCode < 0x1FFFF) {
1070 $smpset = true;
1072 $offset = 0;
1073 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) {
1074 $glyph = $startGlyphCode + $offset;
1075 $offset++;
1076 // ZZZ98
1077 if ($unichar < 0x30000) {
1078 $charToGlyph[$unichar] = $glyph;
1079 $this->maxUniChar = max($unichar, $this->maxUniChar);
1080 $glyphToChar[$glyph][] = $unichar;
1084 } else {
1085 $glyphToChar = [];
1086 $charToGlyph = [];
1087 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
1089 $this->sipset = $sipset;
1090 $this->smpset = $smpset;
1092 ///////////////////////////////////
1093 // mPDF 5.7.1
1094 // Map Unmapped glyphs (or glyphs mapped to upper PUA U+F00000 onwards i.e. > U+2FFFF) - from $numGlyphs
1095 if ($this->useOTL) {
1096 $bctr = 0xE000;
1097 for ($gid = 1; $gid < $numGlyphs; $gid++) {
1098 if (!isset($glyphToChar[$gid])) {
1099 while (isset($charToGlyph[$bctr])) {
1100 $bctr++;
1101 } // Avoid overwriting a glyph already mapped in PUA
1102 // ZZZ98
1103 if (($bctr > 0xF8FF) && ($bctr < 0x2CEB0)) {
1104 if (!$BMPonly) {
1105 $bctr = 0x2CEB0; // Use unassigned area 0x2CEB0 to 0x2F7FF (space for 10,000 characters)
1106 $this->sipset = $sipset = true; // forces subsetting; also ensure charwidths are saved
1107 while (isset($charToGlyph[$bctr])) {
1108 $bctr++;
1110 } else {
1111 throw new \Mpdf\MpdfException($names[1] . " : WARNING - The font does not have enough space to map all (unmapped) included glyphs into Private Use Area U+E000 - U+F8FF");
1114 $glyphToChar[$gid][] = $bctr;
1115 $charToGlyph[$bctr] = $gid;
1116 $this->maxUniChar = max($bctr, $this->maxUniChar);
1117 $bctr++;
1122 $this->glyphToChar = $glyphToChar;
1123 ///////////////////////////////////
1124 // mPDF 5.7.1 OpenType Layout tables
1125 $this->GSUBScriptLang = [];
1126 $this->rtlPUAstr = '';
1127 //$this->rtlPUAarr = array();
1128 if ($useOTL) {
1129 $this->_getGDEFtables();
1130 list($this->GSUBScriptLang, $this->GSUBFeatures, $this->GSUBLookups, $this->rtlPUAstr) = $this->_getGSUBtables();
1131 // , $this->rtlPUAarr not needed
1132 list($this->GPOSScriptLang, $this->GPOSFeatures, $this->GPOSLookups) = $this->_getGPOStables();
1133 $this->glyphIDtoUni = str_pad('', 256 * 256 * 3, "\x00");
1134 foreach ($glyphToChar as $gid => $arr) {
1135 if (isset($glyphToChar[$gid][0])) {
1136 $char = $glyphToChar[$gid][0];
1138 if ($char != 0 && $char != 65535) {
1139 $this->glyphIDtoUni[$gid * 3] = chr($char >> 16);
1140 $this->glyphIDtoUni[$gid * 3 + 1] = chr(($char >> 8) & 0xFF);
1141 $this->glyphIDtoUni[$gid * 3 + 2] = chr($char & 0xFF);
1146 ///////////////////////////////////
1147 // if xHeight and/or CapHeight are not available from OS/2 (e.g. eraly versions)
1148 // Calculate from yMax of 'x' or 'H' Glyphs...
1149 if ($this->xHeight == 0) {
1150 if (isset($charToGlyph[0x78])) {
1151 $gidx = $charToGlyph[0x78]; // U+0078 (LATIN SMALL LETTER X)
1152 $start = $this->seek_table('loca');
1153 if ($indexToLocFormat == 0) {
1154 $this->skip($gidx * 2);
1155 $locax = $this->read_ushort() * 2;
1156 } else if ($indexToLocFormat == 1) {
1157 $this->skip($gidx * 4);
1158 $locax = $this->read_ulong();
1160 $start = $this->seek_table('glyf');
1161 $this->skip($locax);
1162 $this->skip(8);
1163 $yMaxx = $this->read_short();
1164 $this->xHeight = $yMaxx * $scale;
1167 if ($this->capHeight == 0) {
1168 if (isset($charToGlyph[0x48])) {
1169 $gidH = $charToGlyph[0x48]; // U+0048 (LATIN CAPITAL LETTER H)
1170 $start = $this->seek_table('loca');
1171 if ($indexToLocFormat == 0) {
1172 $this->skip($gidH * 2);
1173 $locaH = $this->read_ushort() * 2;
1174 } else if ($indexToLocFormat == 1) {
1175 $this->skip($gidH * 4);
1176 $locaH = $this->read_ulong();
1178 $start = $this->seek_table('glyf');
1179 $this->skip($locaH);
1180 $this->skip(8);
1181 $yMaxH = $this->read_short();
1182 $this->capHeight = $yMaxH * $scale;
1183 } else {
1184 $this->capHeight = $this->ascent;
1185 } // final default is to set it = to Ascent
1188 ///////////////////////////////////
1189 // hmtx - Horizontal metrics table
1190 ///////////////////////////////////
1191 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale);
1193 ///////////////////////////////////
1194 // kern - Kerning pair table
1195 ///////////////////////////////////
1196 // Recognises old form of Kerning table - as required by Windows - Format 0 only
1197 $kern_offset = $this->seek_table("kern");
1198 $version = $this->read_ushort();
1199 $nTables = $this->read_ushort();
1200 // subtable header
1201 $sversion = $this->read_ushort();
1202 $slength = $this->read_ushort();
1203 $scoverage = $this->read_ushort();
1204 $format = $scoverage >> 8;
1205 if ($kern_offset && $version == 0 && $format == 0) {
1206 // Format 0
1207 $nPairs = $this->read_ushort();
1208 $this->skip(6);
1209 for ($i = 0; $i < $nPairs; $i++) {
1210 $left = $this->read_ushort();
1211 $right = $this->read_ushort();
1212 $val = $this->read_short();
1213 if (isset($glyphToChar[$left]) && count($glyphToChar[$left]) == 1 && isset($glyphToChar[$right]) && count($glyphToChar[$right]) == 1) {
1214 if ($left != 32 && $right != 32) {
1215 $this->kerninfo[$glyphToChar[$left][0]][$glyphToChar[$right][0]] = intval($val * $scale);
1222 /////////////////////////////////////////////////////////////////////////////////////////
1223 function _getGDEFtables()
1225 ///////////////////////////////////
1226 // GDEF - Glyph Definition
1227 ///////////////////////////////////
1228 // http://www.microsoft.com/typography/otspec/gdef.htm
1229 if (isset($this->tables["GDEF"])) {
1230 $gdef_offset = $this->seek_table("GDEF");
1231 // ULONG Version of the GDEF table-currently 0x00010000
1232 $ver_maj = $this->read_ushort();
1233 $ver_min = $this->read_ushort();
1234 $GlyphClassDef_offset = $this->read_ushort();
1235 $AttachList_offset = $this->read_ushort();
1236 $LigCaretList_offset = $this->read_ushort();
1237 $MarkAttachClassDef_offset = $this->read_ushort();
1238 // Version 0x00010002 of GDEF header contains additional Offset to a list defining mark glyph set definitions (MarkGlyphSetDef)
1239 if ($ver_min == 2) {
1240 $MarkGlyphSetsDef_offset = $this->read_ushort();
1243 // GlyphClassDef
1244 if ($GlyphClassDef_offset) {
1245 $this->seek($gdef_offset + $GlyphClassDef_offset);
1247 1 Base glyph (single character, spacing glyph)
1248 2 Ligature glyph (multiple character, spacing glyph)
1249 3 Mark glyph (non-spacing combining glyph)
1250 4 Component glyph (part of single character, spacing glyph)
1252 $GlyphByClass = $this->_getClassDefinitionTable();
1253 } else {
1254 $GlyphByClass = [];
1257 if (isset($GlyphByClass[1]) && count($GlyphByClass[1]) > 0) {
1258 $this->GlyphClassBases = ' ' . implode('| ', $GlyphByClass[1]);
1259 } else {
1260 $this->GlyphClassBases = '';
1262 if (isset($GlyphByClass[2]) && count($GlyphByClass[2]) > 0) {
1263 $this->GlyphClassLigatures = ' ' . implode('| ', $GlyphByClass[2]);
1264 } else {
1265 $this->GlyphClassLigatures = '';
1267 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) {
1268 $this->GlyphClassMarks = ' ' . implode('| ', $GlyphByClass[3]);
1269 } else {
1270 $this->GlyphClassMarks = '';
1272 if (isset($GlyphByClass[4]) && count($GlyphByClass[4]) > 0) {
1273 $this->GlyphClassComponents = ' ' . implode('| ', $GlyphByClass[4]);
1274 } else {
1275 $this->GlyphClassComponents = '';
1278 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) {
1279 $Marks = $GlyphByClass[3];
1280 } // to use for MarkAttachmentType
1281 else {
1282 $Marks = [];
1285 /* Required for GPOS
1286 // Attachment List
1287 if ($AttachList_offset) {
1288 $this->seek($gdef_offset+$AttachList_offset );
1290 The Attachment Point List table (AttachmentList) identifies all the attachment points defined in the GPOS table and their associated glyphs so a client can quickly access coordinates for each glyph's attachment points. As a result, the client can cache coordinates for attachment points along with glyph bitmaps and avoid recalculating the attachment points each time it displays a glyph. Without this table, processing speed would be slower because the client would have to decode the GPOS lookups that define attachment points and compile the points in a list.
1292 The Attachment List table (AttachList) may be used to cache attachment point coordinates along with glyph bitmaps.
1294 The table consists of an offset to a Coverage table (Coverage) listing all glyphs that define attachment points in the GPOS table, a count of the glyphs with attachment points (GlyphCount), and an array of offsets to AttachPoint tables (AttachPoint). The array lists the AttachPoint tables, one for each glyph in the Coverage table, in the same order as the Coverage Index.
1295 AttachList table
1296 Type Name Description
1297 Offset Coverage Offset to Coverage table - from beginning of AttachList table
1298 uint16 GlyphCount Number of glyphs with attachment points
1299 Offset AttachPoint[GlyphCount] Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order
1301 An AttachPoint table consists of a count of the attachment points on a single glyph (PointCount) and an array of contour indices of those points (PointIndex), listed in increasing numerical order.
1303 AttachPoint table
1304 Type Name Description
1305 uint16 PointCount Number of attachment points on this glyph
1306 uint16 PointIndex[PointCount] Array of contour point indices -in increasing numerical order
1308 See Example 3 - http://www.microsoft.com/typography/otspec/gdef.htm
1311 // Ligature Caret List
1312 // The Ligature Caret List table (LigCaretList) defines caret positions for all the ligatures in a font.
1313 // Not required for mDPF
1314 // MarkAttachmentType
1315 if ($MarkAttachClassDef_offset) {
1316 $this->seek($gdef_offset + $MarkAttachClassDef_offset);
1317 $MarkAttachmentTypes = $this->_getClassDefinitionTable();
1318 foreach ($MarkAttachmentTypes as $class => $glyphs) {
1319 if (is_array($Marks) && count($Marks)) {
1320 $mat = array_diff($Marks, $MarkAttachmentTypes[$class]);
1321 sort($mat, SORT_STRING);
1322 } else {
1323 $mat = [];
1326 $this->MarkAttachmentType[$class] = ' ' . implode('| ', $mat);
1328 } else {
1329 $this->MarkAttachmentType = [];
1332 // MarkGlyphSets only in Version 0x00010002 of GDEF
1333 if ($ver_min == 2 && $MarkGlyphSetsDef_offset) {
1334 $this->seek($gdef_offset + $MarkGlyphSetsDef_offset);
1335 $MarkSetTableFormat = $this->read_ushort();
1336 $MarkSetCount = $this->read_ushort();
1337 $MarkSetOffset = [];
1338 for ($i = 0; $i < $MarkSetCount; $i++) {
1339 $MarkSetOffset[] = $this->read_ulong();
1341 for ($i = 0; $i < $MarkSetCount; $i++) {
1342 $this->seek($MarkSetOffset[$i]);
1343 $glyphs = $this->_getCoverage();
1344 $this->MarkGlyphSets[$i] = ' ' . implode('| ', $glyphs);
1346 } else {
1347 $this->MarkGlyphSets = [];
1349 } else {
1350 throw new \Mpdf\MpdfException('Warning - You cannot set this font (' . $this->filename . ') to use OTL, as it does not include OTL tables (or at least, not a GDEF table).');
1353 //=====================================================================================
1354 //=====================================================================================
1355 //=====================================================================================
1356 $GSUB_offset = 0;
1357 $GPOS_offset = 0;
1358 $GSUB_length = 0;
1359 $s = '';
1360 if (isset($this->tables["GSUB"])) {
1361 $GSUB_offset = $this->seek_table("GSUB");
1362 $GSUB_length = $this->tables["GSUB"]['length'];
1363 $s .= fread($this->fh, $this->tables["GSUB"]['length']);
1365 if (isset($this->tables["GPOS"])) {
1366 $GPOS_offset = $this->seek_table("GPOS");
1367 $s .= fread($this->fh, $this->tables["GPOS"]['length']);
1369 if ($s) {
1370 $this->fontCache->write($this->fontkey . '.GSUBGPOStables.dat', $s);
1373 //=====================================================================================
1374 //=====================================================================================
1376 $s = '<?php
1377 $GSUB_offset = ' . $GSUB_offset . ';
1378 $GPOS_offset = ' . $GPOS_offset . ';
1379 $GSUB_length = ' . $GSUB_length . ';
1380 $GlyphClassBases = \'' . $this->GlyphClassBases . '\';
1381 $GlyphClassMarks = \'' . $this->GlyphClassMarks . '\';
1382 $GlyphClassLigatures = \'' . $this->GlyphClassLigatures . '\';
1383 $GlyphClassComponents = \'' . $this->GlyphClassComponents . '\';
1384 $MarkGlyphSets = ' . var_export($this->MarkGlyphSets, true) . ';
1385 $MarkAttachmentType = ' . var_export($this->MarkAttachmentType, true) . ';
1388 $this->fontCache->write($this->fontkey . '.GDEFdata.php', $s);
1391 function _getClassDefinitionTable()
1394 // NB Any glyph not included in the range of covered GlyphIDs automatically belongs to Class 0. This is not returned by this function
1395 $ClassFormat = $this->read_ushort();
1396 $GlyphByClass = [];
1397 if ($ClassFormat == 1) {
1398 $StartGlyph = $this->read_ushort();
1399 $GlyphCount = $this->read_ushort();
1400 for ($i = 0; $i < $GlyphCount; $i++) {
1401 $gid = $StartGlyph + $i;
1402 $class = $this->read_ushort();
1403 // Several fonts (mainly dejavu.../Freeserif etc) have a MarkAttachClassDef Format 1, where StartGlyph is 0 and GlyphCount is 1
1404 // This doesn't seem to do anything useful?
1405 // Freeserif does not have $this->glyphToChar[0] allocated and would throw an error, so check if isset:
1406 if (isset($this->glyphToChar[$gid][0])) {
1407 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]);
1410 } else if ($ClassFormat == 2) {
1411 $tableCount = $this->read_ushort();
1412 for ($i = 0; $i < $tableCount; $i++) {
1413 $startGlyphID = $this->read_ushort();
1414 $endGlyphID = $this->read_ushort();
1415 $class = $this->read_ushort();
1416 for ($gid = $startGlyphID; $gid <= $endGlyphID; $gid++) {
1417 if (isset($this->glyphToChar[$gid][0])) {
1418 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]);
1423 foreach ($GlyphByClass as $class => $glyphs) {
1424 sort($GlyphByClass[$class], SORT_STRING); // SORT makes it easier to read in development ? order not important ???
1426 ksort($GlyphByClass);
1428 return $GlyphByClass;
1431 function _getGSUBtables()
1433 ///////////////////////////////////
1434 // GSUB - Glyph Substitution
1435 ///////////////////////////////////
1436 if (isset($this->tables["GSUB"])) {
1437 $ffeats = [];
1438 $gsub_offset = $this->seek_table("GSUB");
1439 $this->skip(4);
1440 $ScriptList_offset = $gsub_offset + $this->read_ushort();
1441 $FeatureList_offset = $gsub_offset + $this->read_ushort();
1442 $LookupList_offset = $gsub_offset + $this->read_ushort();
1444 // ScriptList
1445 $this->seek($ScriptList_offset);
1446 $ScriptCount = $this->read_ushort();
1447 for ($i = 0; $i < $ScriptCount; $i++) {
1448 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc.
1449 $ScriptTableOffset = $this->read_ushort();
1450 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset;
1453 // Script Table
1454 foreach ($ffeats as $t => $o) {
1455 $ls = [];
1456 $this->seek($o);
1457 $DefLangSys_offset = $this->read_ushort();
1458 if ($DefLangSys_offset > 0) {
1459 $ls['DFLT'] = $DefLangSys_offset + $o;
1461 $LangSysCount = $this->read_ushort();
1462 for ($i = 0; $i < $LangSysCount; $i++) {
1463 $LangTag = $this->read_tag(); // =
1464 $LangTableOffset = $this->read_ushort();
1465 $ls[$LangTag] = $o + $LangTableOffset;
1467 $ffeats[$t] = $ls;
1469 //print_r($ffeats); exit;
1470 // Get FeatureIndexList
1471 // LangSys Table - from first listed langsys
1472 foreach ($ffeats as $st => $scripts) {
1473 foreach ($scripts as $t => $o) {
1474 $FeatureIndex = [];
1475 $langsystable_offset = $o;
1476 $this->seek($langsystable_offset);
1477 $LookUpOrder = $this->read_ushort(); //==NULL
1478 $ReqFeatureIndex = $this->read_ushort();
1479 if ($ReqFeatureIndex != 0xFFFF) {
1480 $FeatureIndex[] = $ReqFeatureIndex;
1482 $FeatureCount = $this->read_ushort();
1483 for ($i = 0; $i < $FeatureCount; $i++) {
1484 $FeatureIndex[] = $this->read_ushort(); // = index of feature
1486 $ffeats[$st][$t] = $FeatureIndex;
1489 //print_r($ffeats); exit;
1490 // Feauture List => LookupListIndex es
1491 $this->seek($FeatureList_offset);
1492 $FeatureCount = $this->read_ushort();
1493 $Feature = [];
1494 for ($i = 0; $i < $FeatureCount; $i++) {
1495 $tag = $this->read_tag();
1496 if ($tag == 'smcp') {
1497 $this->hassmallcapsGSUB = true;
1499 $Feature[$i] = ['tag' => $tag];
1500 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort();
1502 for ($i = 0; $i < $FeatureCount; $i++) {
1503 $this->seek($Feature[$i]['offset']);
1504 $this->read_ushort(); // null [FeatureParams]
1505 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort();
1506 $Feature[$i]['LookupListIndex'] = [];
1507 for ($c = 0; $c < $Lookupcount; $c++) {
1508 $Feature[$i]['LookupListIndex'][] = $this->read_ushort();
1512 //print_r($Feature); exit;
1514 foreach ($ffeats as $st => $scripts) {
1515 foreach ($scripts as $t => $o) {
1516 $FeatureIndex = $ffeats[$st][$t];
1517 foreach ($FeatureIndex as $k => $fi) {
1518 $ffeats[$st][$t][$k] = $Feature[$fi];
1522 //=====================================================================================
1523 $gsub = [];
1524 $GSUBScriptLang = [];
1525 foreach ($ffeats as $st => $scripts) {
1526 foreach ($scripts as $t => $langsys) {
1527 $lg = [];
1528 foreach ($langsys as $ft) {
1529 $lg[$ft['LookupListIndex'][0]] = $ft;
1531 // list of Lookups in order they need to be run i.e. order listed in Lookup table
1532 ksort($lg);
1533 foreach ($lg as $ft) {
1534 $gsub[$st][$t][$ft['tag']] = $ft['LookupListIndex'];
1536 if (!isset($GSUBScriptLang[$st])) {
1537 $GSUBScriptLang[$st] = '';
1539 $GSUBScriptLang[$st] .= $t . ' ';
1543 //print_r($gsub); exit;
1544 //=====================================================================================
1545 // Get metadata and offsets for whole Lookup List table
1546 $this->seek($LookupList_offset);
1547 $LookupCount = $this->read_ushort();
1548 $GSLookup = [];
1549 $Offsets = [];
1550 $SubtableCount = [];
1551 for ($i = 0; $i < $LookupCount; $i++) {
1552 $Offsets[$i] = $LookupList_offset + $this->read_ushort();
1554 for ($i = 0; $i < $LookupCount; $i++) {
1555 $this->seek($Offsets[$i]);
1556 $GSLookup[$i]['Type'] = $this->read_ushort();
1557 $GSLookup[$i]['Flag'] = $flag = $this->read_ushort();
1558 $GSLookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort();
1559 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
1560 $GSLookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort();
1562 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
1563 if (($flag & 0x0010) == 0x0010) {
1564 $GSLookup[$i]['MarkFilteringSet'] = $this->read_ushort();
1565 } else {
1566 $GSLookup[$i]['MarkFilteringSet'] = '';
1569 // Lookup Type 7: Extension
1570 if ($GSLookup[$i]['Type'] == 7) {
1571 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
1572 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
1573 $this->seek($GSLookup[$i]['Subtables'][$c]);
1574 $ExtensionPosFormat = $this->read_ushort();
1575 $type = $this->read_ushort();
1576 $ext_offset = $this->read_ulong();
1577 $GSLookup[$i]['Subtables'][$c] = $GSLookup[$i]['Subtables'][$c] + $ext_offset;
1579 $GSLookup[$i]['Type'] = $type;
1583 //print_r($GSLookup); exit;
1584 //=====================================================================================
1585 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph
1586 $this->GSLuCoverage = [];
1587 for ($i = 0; $i < $LookupCount; $i++) {
1588 for ($c = 0; $c < $GSLookup[$i]['SubtableCount']; $c++) {
1589 $this->seek($GSLookup[$i]['Subtables'][$c]);
1590 $PosFormat = $this->read_ushort();
1592 if ($GSLookup[$i]['Type'] == 5 && $PosFormat == 3) {
1593 $this->skip(4);
1594 } else if ($GSLookup[$i]['Type'] == 6 && $PosFormat == 3) {
1595 $BacktrackGlyphCount = $this->read_ushort();
1596 $this->skip(2 * $BacktrackGlyphCount + 2);
1598 // NB Coverage only looks at glyphs for position 1 (i.e. 5.3 and 6.3) // NEEDS TO READ ALL ********************
1599 $Coverage = $GSLookup[$i]['Subtables'][$c] + $this->read_ushort();
1600 $this->seek($Coverage);
1601 $glyphs = $this->_getCoverage(false, 2);
1602 $this->GSLuCoverage[$i][$c] = $glyphs;
1606 // $this->GSLuCoverage and $GSLookup
1608 $s = '<?php
1609 $GSLuCoverage = ' . var_export($this->GSLuCoverage, true) . ';
1612 $this->fontCache->write($this->fontkey . '.GSUBdata.php', $s);
1614 // Now repeats as original to get Substitution rules
1616 // Get metadata and offsets for whole Lookup List table
1617 $this->seek($LookupList_offset);
1618 $LookupCount = $this->read_ushort();
1619 $Lookup = [];
1621 for ($i = 0; $i < $LookupCount; $i++) {
1622 $Lookup[$i]['offset'] = $LookupList_offset + $this->read_ushort();
1625 for ($i = 0; $i < $LookupCount; $i++) {
1626 $this->seek($Lookup[$i]['offset']);
1627 $Lookup[$i]['Type'] = $this->read_ushort();
1628 $Lookup[$i]['Flag'] = $flag = $this->read_ushort();
1629 $Lookup[$i]['SubtableCount'] = $this->read_ushort();
1630 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1631 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['offset'] + $this->read_ushort();
1633 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
1634 if (($flag & 0x0010) == 0x0010) {
1635 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort();
1636 } else {
1637 $Lookup[$i]['MarkFilteringSet'] = '';
1640 // Lookup Type 7: Extension
1641 if ($Lookup[$i]['Type'] == 7) {
1642 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
1643 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1644 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']);
1645 $ExtensionPosFormat = $this->read_ushort();
1646 $type = $this->read_ushort();
1647 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ulong();
1649 $Lookup[$i]['Type'] = $type;
1653 //print_r($Lookup); exit;
1654 //=====================================================================================
1655 // Process (1) Whole LookupList
1656 for ($i = 0; $i < $LookupCount; $i++) {
1657 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1658 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']);
1659 $SubstFormat = $this->read_ushort();
1660 $Lookup[$i]['Subtable'][$c]['Format'] = $SubstFormat;
1663 Lookup['Type'] Enumeration table for glyph substitution
1664 Value Type Description
1665 1 Single Replace one glyph with one glyph
1666 2 Multiple Replace one glyph with more than one glyph
1667 3 Alternate Replace one glyph with one of many glyphs
1668 4 Ligature Replace multiple glyphs with one glyph
1669 5 Context Replace one or more glyphs in context
1670 6 Chaining Context Replace one or more glyphs in chained context
1671 7 Extension Substitution Extension mechanism for other substitutions (i.e. this excludes the Extension type substitution itself)
1672 8 Reverse chaining context single Applied in reverse order, replace single glyph in chaining context
1675 // LookupType 1: Single Substitution Subtable
1676 if ($Lookup[$i]['Type'] == 1) {
1677 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1678 if ($SubstFormat == 1) { // Calculated output glyph indices
1679 $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'] = $this->read_short();
1680 } else if ($SubstFormat == 2) { // Specified output glyph indices
1681 $GlyphCount = $this->read_ushort();
1682 for ($g = 0; $g < $GlyphCount; $g++) {
1683 $Lookup[$i]['Subtable'][$c]['Glyphs'][] = $this->read_ushort();
1686 } // LookupType 2: Multiple Substitution Subtable
1687 else if ($Lookup[$i]['Type'] == 2) {
1688 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1689 $Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->read_short();
1690 for ($s = 0; $s < $SequenceCount; $s++) {
1691 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1693 for ($s = 0; $s < $SequenceCount; $s++) {
1694 // Sequence Tables
1695 $this->seek($Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset']);
1696 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->read_short();
1697 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount']; $g++) {
1698 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['SubstituteGlyphID'][] = $this->read_ushort();
1701 } // LookupType 3: Alternate Forms
1702 else if ($Lookup[$i]['Type'] == 3) {
1703 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1704 $Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->read_short();
1705 for ($s = 0; $s < $AlternateSetCount; $s++) {
1706 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1709 for ($s = 0; $s < $AlternateSetCount; $s++) {
1710 // AlternateSet Tables
1711 $this->seek($Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset']);
1712 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->read_short();
1713 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount']; $g++) {
1714 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['SubstituteGlyphID'][] = $this->read_ushort();
1717 } // LookupType 4: Ligature Substitution Subtable
1718 else if ($Lookup[$i]['Type'] == 4) {
1719 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1720 $Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->read_short();
1721 for ($s = 0; $s < $LigSetCount; $s++) {
1722 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1724 for ($s = 0; $s < $LigSetCount; $s++) {
1725 // LigatureSet Tables
1726 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset']);
1727 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->read_short();
1728 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1729 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g] = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] + $this->read_ushort();
1732 for ($s = 0; $s < $LigSetCount; $s++) {
1733 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1734 // Ligature tables
1735 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g]);
1736 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'] = $this->read_ushort();
1737 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount'] = $this->read_ushort();
1738 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) {
1739 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l] = $this->read_ushort();
1743 } // LookupType 5: Contextual Substitution Subtable
1744 else if ($Lookup[$i]['Type'] == 5) {
1745 // Format 1: Context Substitution
1746 if ($SubstFormat == 1) {
1747 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1748 $Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->read_short();
1749 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1750 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1752 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1753 // SubRuleSet Tables
1754 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset']);
1755 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->read_short();
1756 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) {
1757 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] + $this->read_ushort();
1760 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1761 // SubRule Tables
1762 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) {
1763 // Ligature tables
1764 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g]);
1766 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount'] = $this->read_ushort();
1767 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount'] = $this->read_ushort();
1768 // "Input"::[GlyphCount - 1]::Array of input GlyphIDs-start with second glyph
1769 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount']; $l++) {
1770 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['Input'][$l] = $this->read_ushort();
1772 // "SubstLookupRecord"::[SubstCount]::Array of SubstLookupRecords-in design order
1773 for ($l = 0; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount']; $l++) {
1774 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['SequenceIndex'] = $this->read_ushort();
1775 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['LookupListIndex'] = $this->read_ushort();
1779 } // Format 2: Class-based Context Glyph Substitution
1780 else if ($SubstFormat == 2) {
1781 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1782 $Lookup[$i]['Subtable'][$c]['ClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1783 $Lookup[$i]['Subtable'][$c]['SubClassSetCnt'] = $this->read_ushort();
1784 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $b++) {
1785 $offset = $this->read_ushort();
1786 if ($offset == 0x0000) {
1787 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = 0;
1788 } else {
1789 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset;
1792 } else {
1793 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Lookup[$i]['Type'] . ", Format " . $SubstFormat . " not supported (ttfontsuni.php).");
1795 } // LookupType 6: Chaining Contextual Substitution Subtable
1796 else if ($Lookup[$i]['Type'] == 6) {
1797 // Format 1: Simple Chaining Context Glyph Substitution p255
1798 if ($SubstFormat == 1) {
1799 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1800 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'] = $this->read_ushort();
1801 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $b++) {
1802 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1804 } // Format 2: Class-based Chaining Context Glyph Substitution p257
1805 else if ($SubstFormat == 2) {
1806 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1807 $Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1808 $Lookup[$i]['Subtable'][$c]['InputClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1809 $Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1810 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt'] = $this->read_ushort();
1811 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $b++) {
1812 $offset = $this->read_ushort();
1813 if ($offset == 0x0000) {
1814 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $offset;
1815 } else {
1816 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset;
1819 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
1820 else if ($SubstFormat == 3) {
1821 $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount'] = $this->read_ushort();
1822 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) {
1823 $Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1825 $Lookup[$i]['Subtable'][$c]['InputGlyphCount'] = $this->read_ushort();
1826 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
1827 $Lookup[$i]['Subtable'][$c]['CoverageInput'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1829 $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount'] = $this->read_ushort();
1830 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) {
1831 $Lookup[$i]['Subtable'][$c]['CoverageLookahead'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1833 $Lookup[$i]['Subtable'][$c]['SubstCount'] = $this->read_ushort();
1834 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
1835 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'] = $this->read_ushort();
1836 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'] = $this->read_ushort();
1838 Substitution Lookup Record
1839 All contextual substitution subtables specify the substitution data in a Substitution Lookup Record (SubstLookupRecord). Each record contains a SequenceIndex, which indicates the position where the substitution will occur in the glyph sequence. In addition, a LookupListIndex identifies the lookup to be applied at the glyph position specified by the SequenceIndex.
1843 } else {
1844 throw new \Mpdf\MpdfException("Lookup Type " . $Lookup[$i]['Type'] . " not supported.");
1848 //print_r($Lookup); exit;
1849 //=====================================================================================
1850 // Process (2) Whole LookupList
1851 // Get Coverage tables and prepare preg_replace
1852 for ($i = 0; $i < $LookupCount; $i++) {
1853 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1854 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format'];
1856 // LookupType 1: Single Substitution Subtable 1 => 1
1857 if ($Lookup[$i]['Type'] == 1) {
1858 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1859 $glyphs = $this->_getCoverage(false);
1860 for ($g = 0; $g < count($glyphs); $g++) {
1861 $replace = [];
1862 $substitute = [];
1863 $replace[] = unicode_hex($this->glyphToChar[$glyphs[$g]][0]);
1864 // Flag = Ignore
1865 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1866 continue;
1868 if (isset($Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])) { // Format 1
1869 $substitute[] = unicode_hex($this->glyphToChar[($glyphs[$g] + $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])][0]);
1870 } else { // Format 2
1871 $substitute[] = unicode_hex($this->glyphToChar[($Lookup[$i]['Subtable'][$c]['Glyphs'][$g])][0]);
1873 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1875 } // LookupType 2: Multiple Substitution Subtable 1 => n
1876 else if ($Lookup[$i]['Type'] == 2) {
1877 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1878 $glyphs = $this->_getCoverage();
1879 for ($g = 0; $g < count($glyphs); $g++) {
1880 $replace = [];
1881 $substitute = [];
1882 $replace[] = $glyphs[$g];
1883 // Flag = Ignore
1884 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1885 continue;
1887 if (!isset($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) || count($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) == 0) {
1888 continue;
1889 } // Illegal for GlyphCount to be 0; either error in font, or something has gone wrong - lets carry on for now!
1890 foreach ($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID'] as $sub) {
1891 $substitute[] = unicode_hex($this->glyphToChar[$sub][0]);
1893 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1895 } // LookupType 3: Alternate Forms 1 => 1 (only first alternate form is used)
1896 else if ($Lookup[$i]['Type'] == 3) {
1897 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1898 $glyphs = $this->_getCoverage();
1899 for ($g = 0; $g < count($glyphs); $g++) {
1900 $replace = [];
1901 $substitute = [];
1902 $replace[] = $glyphs[$g];
1903 // Flag = Ignore
1904 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1905 continue;
1907 $gid = $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['SubstituteGlyphID'][0];
1908 if (!isset($this->glyphToChar[$gid][0])) {
1909 continue;
1911 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]);
1912 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1914 } // LookupType 4: Ligature Substitution Subtable n => 1
1915 else if ($Lookup[$i]['Type'] == 4) {
1916 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1917 $glyphs = $this->_getCoverage();
1918 $LigSetCount = $Lookup[$i]['Subtable'][$c]['LigSetCount'];
1919 for ($s = 0; $s < $LigSetCount; $s++) {
1920 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1921 $replace = [];
1922 $substitute = [];
1923 $replace[] = $glyphs[$s];
1924 // Flag = Ignore
1925 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1926 continue;
1928 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) {
1929 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l];
1930 $rpl = unicode_hex($this->glyphToChar[$gid][0]);
1931 // Flag = Ignore
1932 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $rpl, $Lookup[$i]['MarkFilteringSet'])) {
1933 continue 2;
1935 $replace[] = $rpl;
1937 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'];
1938 if (!isset($this->glyphToChar[$gid][0])) {
1939 continue;
1941 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]);
1942 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute, 'CompCount' => $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']];
1945 } // LookupType 5: Contextual Substitution Subtable
1946 else if ($Lookup[$i]['Type'] == 5) {
1947 // Format 1: Context Substitution
1948 if ($SubstFormat == 1) {
1949 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1950 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1952 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) {
1953 $SubRuleSet = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s];
1954 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'] = $CoverageGlyphs[$s];
1955 for ($r = 0; $r < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $r++) {
1956 $GlyphCount = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['GlyphCount'];
1957 for ($g = 1; $g < $GlyphCount; $g++) {
1958 $glyphID = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['Input'][$g];
1959 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
1963 } // Format 2: Class-based Context Glyph Substitution
1964 else if ($SubstFormat == 2) {
1965 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1966 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1968 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['ClassDefOffset']);
1969 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses;
1970 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) {
1971 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) {
1972 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s]);
1973 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'] = $SubClassRuleCnt = $this->read_ushort();
1974 $SubClassRule = [];
1975 for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1976 $SubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] + $this->read_ushort();
1977 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $SubClassRule[$b];
1982 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) {
1983 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) {
1984 $SubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'];
1985 for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1986 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b]);
1987 $Rule = [];
1988 $Rule['InputGlyphCount'] = $this->read_ushort();
1989 $Rule['SubstCount'] = $this->read_ushort();
1990 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) {
1991 $Rule['Input'][$r] = $this->read_ushort();
1993 for ($r = 0; $r < $Rule['SubstCount']; $r++) {
1994 $Rule['SequenceIndex'][$r] = $this->read_ushort();
1995 $Rule['LookupListIndex'][$r] = $this->read_ushort();
1998 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $Rule;
2002 } // Format 3: Coverage-based Context Glyph Substitution
2003 else if ($SubstFormat == 3) {
2004 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
2005 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]);
2006 $glyphs = $this->_getCoverage();
2007 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs);
2009 throw new \Mpdf\MpdfException("Lookup Type 5, SubstFormat 3 not tested. Please report this with the name of font used - " . $this->fontkey);
2011 } // LookupType 6: Chaining Contextual Substitution Subtable
2012 else if ($Lookup[$i]['Type'] == 6) {
2013 // Format 1: Simple Chaining Context Glyph Substitution p255
2014 if ($SubstFormat == 1) {
2015 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
2016 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
2018 $ChainSubRuleSetCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'];
2020 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) {
2021 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s]);
2022 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'] = $this->read_ushort();
2023 for ($r = 0; $r < $ChainSubRuleCnt; $r++) {
2024 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r] = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s] + $this->read_ushort();
2027 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) {
2028 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'];
2029 for ($r = 0; $r < $ChainSubRuleCnt; $r++) {
2030 // ChainSubRule
2031 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r]);
2033 $BacktrackGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphCount'] = $this->read_ushort();
2034 for ($g = 0; $g < $BacktrackGlyphCount; $g++) {
2035 $glyphID = $this->read_ushort();
2036 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
2039 $InputGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphCount'] = $this->read_ushort();
2040 for ($g = 1; $g < $InputGlyphCount; $g++) {
2041 $glyphID = $this->read_ushort();
2042 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
2045 $LookaheadGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphCount'] = $this->read_ushort();
2046 for ($g = 0; $g < $LookaheadGlyphCount; $g++) {
2047 $glyphID = $this->read_ushort();
2048 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
2051 $SubstCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SubstCount'] = $this->read_ushort();
2052 for ($lu = 0; $lu < $SubstCount; $lu++) {
2053 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SequenceIndex'][$lu] = $this->read_ushort();
2054 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookupListIndex'][$lu] = $this->read_ushort();
2058 } // Format 2: Class-based Chaining Context Glyph Substitution p257
2059 else if ($SubstFormat == 2) {
2060 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
2061 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
2063 $BacktrackClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset']);
2064 $Lookup[$i]['Subtable'][$c]['BacktrackClasses'] = $BacktrackClasses;
2066 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['InputClassDefOffset']);
2067 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses;
2069 $LookaheadClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset']);
2070 $Lookup[$i]['Subtable'][$c]['LookaheadClasses'] = $LookaheadClasses;
2072 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) {
2073 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) {
2074 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s]);
2075 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'] = $ChainSubClassRuleCnt = $this->read_ushort();
2076 $ChainSubClassRule = [];
2077 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
2078 $ChainSubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] + $this->read_ushort();
2079 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $ChainSubClassRule[$b];
2084 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) {
2085 if (isset($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'])) {
2086 $ChainSubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'];
2087 } else {
2088 $ChainSubClassRuleCnt = 0;
2090 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
2091 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) {
2092 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b]);
2093 $Rule = [];
2094 $Rule['BacktrackGlyphCount'] = $this->read_ushort();
2095 for ($r = 0; $r < $Rule['BacktrackGlyphCount']; $r++) {
2096 $Rule['Backtrack'][$r] = $this->read_ushort();
2098 $Rule['InputGlyphCount'] = $this->read_ushort();
2099 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) {
2100 $Rule['Input'][$r] = $this->read_ushort();
2102 $Rule['LookaheadGlyphCount'] = $this->read_ushort();
2103 for ($r = 0; $r < $Rule['LookaheadGlyphCount']; $r++) {
2104 $Rule['Lookahead'][$r] = $this->read_ushort();
2106 $Rule['SubstCount'] = $this->read_ushort();
2107 for ($r = 0; $r < $Rule['SubstCount']; $r++) {
2108 $Rule['SequenceIndex'][$r] = $this->read_ushort();
2109 $Rule['LookupListIndex'][$r] = $this->read_ushort();
2112 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $Rule;
2116 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
2117 else if ($SubstFormat == 3) {
2118 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) {
2119 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][$b]);
2120 $glyphs = $this->_getCoverage();
2121 $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'][] = implode("|", $glyphs);
2123 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
2124 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]);
2125 $glyphs = $this->_getCoverage();
2126 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs);
2127 // Don't use above value as these are ordered numerically not as need to process
2129 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) {
2130 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageLookahead'][$b]);
2131 $glyphs = $this->_getCoverage();
2132 $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'][] = implode("|", $glyphs);
2139 //=====================================================================================
2140 //=====================================================================================
2141 //=====================================================================================
2142 //=====================================================================================
2144 $GSUBScriptLang = [];
2145 $rtlpua = []; // All glyphs added to PUA [for magic_reverse]
2146 foreach ($gsub as $st => $scripts) {
2147 foreach ($scripts as $t => $langsys) {
2148 $lul = []; // array of LookupListIndexes
2149 $tags = []; // corresponding array of feature tags e.g. 'ccmp'
2150 //print_r($langsys ); exit;
2151 foreach ($langsys as $tag => $ft) {
2152 foreach ($ft as $ll) {
2153 $lul[$ll] = $tag;
2156 ksort($lul); // Order the Lookups in the order they are in the GUSB table, regardless of Feature order
2157 $volt = $this->_getGSUBarray($Lookup, $lul, $st);
2158 //print_r($lul); exit;
2159 //=====================================================================================
2160 //=====================================================================================
2161 // Interrogate $volt
2162 // isol, fin, medi, init(arab syrc) into $rtlSUB for use in ArabJoin
2163 // but also identify all RTL chars in PUA for magic_reverse (arab syrc hebr thaa nko samr)
2164 // identify reph, matras, vatu, half forms etc for Indic for final re-ordering
2165 //=====================================================================================
2166 //=====================================================================================
2167 $rtl = [];
2168 $rtlSUB = "array()";
2169 $finals = '';
2170 if (strpos('arab syrc hebr thaa nko samr', $st) !== false) { // all RTL scripts [any/all languages] ? Mandaic
2171 //print_r($volt); exit;
2172 foreach ($volt as $v) {
2173 // isol fina fin2 fin3 medi med2 for Syriac
2174 // ISOLATED FORM :: FINAL :: INITIAL :: MEDIAL :: MED2 :: FIN2 :: FIN3
2175 if (strpos('isol fina init medi fin2 fin3 med2', $v['tag']) !== false) {
2176 $key = $v['match'];
2177 $key = preg_replace('/[\(\)]*/', '', $key);
2178 $sub = $v['replace'];
2179 if ($v['tag'] == 'isol') {
2180 $kk = 0;
2181 } else if ($v['tag'] == 'fina') {
2182 $kk = 1;
2183 } else if ($v['tag'] == 'init') {
2184 $kk = 2;
2185 } else if ($v['tag'] == 'medi') {
2186 $kk = 3;
2187 } else if ($v['tag'] == 'med2') {
2188 $kk = 4;
2189 } else if ($v['tag'] == 'fin2') {
2190 $kk = 5;
2191 } else if ($v['tag'] == 'fin3') {
2192 $kk = 6;
2194 $rtl[$key][$kk] = $sub;
2195 if (isset($v['prel']) && count($v['prel'])) {
2196 $rtl[$key]['prel'][$kk] = $v['prel'];
2198 if (isset($v['postl']) && count($v['postl'])) {
2199 $rtl[$key]['postl'][$kk] = $v['postl'];
2201 if (isset($v['ignore']) && $v['ignore']) {
2202 $rtl[$key]['ignore'][$kk] = $v['ignore'];
2204 $rtlpua[] = $sub;
2205 } // Add any other glyphs which are in PUA
2206 else {
2207 if (isset($v['context']) && $v['context']) {
2208 foreach ($v['rules'] as $vs) {
2209 for ($i = 0; $i < count($vs['match']); $i++) {
2210 if (isset($vs['replace'][$i]) && preg_match('/^0[A-F0-9]{4}$/', $vs['match'][$i])) {
2211 if (preg_match('/^0[EF][A-F0-9]{3}$/', $vs['replace'][$i])) {
2212 $rtlpua[] = $vs['replace'][$i];
2217 } else {
2218 preg_match_all('/\((0[A-F0-9]{4})\)/', $v['match'], $m);
2219 for ($i = 0; $i < count($m[0]); $i++) {
2220 $sb = explode(' ', $v['replace']);
2221 foreach ($sb as $sbg) {
2222 if (preg_match('/(0[EF][A-F0-9]{3})/', $sbg, $mr)) {
2223 $rtlpua[] = $mr[1];
2230 //print_r($rtl); exit;
2231 // For kashida, need to determine all final forms except ones already identified by kashida
2232 // priority rules (see otl.php)
2233 foreach ($rtl as $base => $variants) {
2234 if (isset($variants[1])) { // i.e. final form
2235 if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEAE 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE 0FEF0 0FEF2', $variants[1]) === false) { // not already included
2236 // This version does not exclude RA (0631) FEAE; Ya (064A) FEF2; Alef Maqsurah (0649) FEF0 which
2237 // are selected in priority if connected to a medial Bah
2238 //if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE', $variants[1])===false) { // not already included
2239 $finals .= $variants[1] . ' ';
2243 //echo $finals ; exit;
2244 //print_r($rtlpua); exit;
2245 ksort($rtl);
2246 $a = var_export($rtl, true);
2247 $a = preg_replace('/\\\\\\\\/', "\\", $a);
2248 $a = preg_replace('/\'/', '"', $a);
2249 $a = preg_replace('/\r/', '', $a);
2250 $a = preg_replace('/> \n/', '>', $a);
2251 $a = preg_replace('/\n \)/', ')', $a);
2252 $a = preg_replace('/\n /', ' ', $a);
2253 $a = preg_replace('/\[IGNORE(\d+)\]/', '".$ignore[\\1]."', $a);
2254 $rtlSUB = preg_replace('/[ ]+/', ' ', $a);
2256 //=====================================================================================
2257 // INDIC - Dynamic properties
2258 //=====================================================================================
2259 $rphf = [];
2260 $half = [];
2261 $pref = [];
2262 $blwf = [];
2263 $pstf = [];
2264 if (strpos('dev2 bng2 gur2 gjr2 ory2 tml2 tel2 knd2 mlm2 deva beng guru gujr orya taml telu knda mlym', $st) !== false) { // all INDIC scripts [any/all languages]
2265 if (strpos('deva beng guru gujr orya taml telu knda mlym', $st) !== false) {
2266 $is_old_spec = true;
2267 } else {
2268 $is_old_spec = false;
2271 // First get 'locl' substitutions (reversed!)
2272 $loclsubs = [];
2273 foreach ($volt as $v) {
2274 if (strpos('locl', $v['tag']) !== false) {
2275 $key = $v['match'];
2276 $key = preg_replace('/[\(\)]*/', '', $key);
2277 $sub = $v['replace'];
2278 if ($key && strlen(trim($key)) == 5 && $sub) {
2279 $loclsubs[$sub] = $key;
2283 //if (count($loclsubs)) { print_r($loclsubs); exit; }
2285 foreach ($volt as $v) {
2286 // <rphf> <half> <pref> <blwf> <pstf>
2287 // defines consonant types:
2288 // Reph <rphf>
2289 // Half forms <half>
2290 // Pre-base-reordering forms of Ra/Rra <pref>
2291 // Below-base forms <blwf>
2292 // Post-base forms <pstf>
2293 // applied together with <locl> feature to input sequences consisting of two characters
2294 // This is done for each consonant
2295 // for <rphf> and <half>, features are applied to Consonant + Halant combinations
2296 // for <pref>, <blwf> and <pstf>, features are applied to Halant + Consonant combinations
2297 // Old version eg 'deva' <pref>, <blwf> and <pstf>, features are applied to Consonant + Halant
2298 // Some malformed fonts still do Consonant + Halant for these - so match both??
2299 // If these two glyphs form a ligature, with no additional glyphs in context
2300 // this means the consonant has the corresponding form
2301 // Currently set to cope with both
2302 // See also classes/otl.php
2304 if (strpos('rphf half pref blwf pstf', $v['tag']) !== false) {
2305 if (isset($v['context']) && $v['context'] && $v['nBacktrack'] == 0 && $v['nLookahead'] == 0) {
2306 foreach ($v['rules'] as $vs) {
2307 if (count($vs['match']) == 2 && count($vs['replace']) == 1) {
2308 $sub = $vs['replace'][0];
2309 // If Halant Cons <pref>, <blwf> and <pstf> in New version only
2310 if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $vs['match'][0]) !== false && strpos('pref blwf pstf', $v['tag']) !== false && !$is_old_spec) {
2311 $key = $vs['match'][1];
2312 $tag = $v['tag'];
2313 if (isset($loclsubs[$key])) {
2314 ${$tag[$loclsubs[$key]]} = $sub;
2316 $tmp = &$$tag;
2317 $tmp[hexdec($key)] = hexdec($sub);
2318 } // If Cons Halant <rphf> and <half> always
2319 // and <pref>, <blwf> and <pstf> in Old version
2320 else if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $vs['match'][1]) !== false && (strpos('rphf half', $v['tag']) !== false || (strpos('pref blwf pstf', $v['tag']) !== false && ($is_old_spec || _OTL_OLD_SPEC_COMPAT_2)))) {
2321 $key = $vs['match'][0];
2322 $tag = $v['tag'];
2323 if (isset($loclsubs[$key])) {
2324 ${$tag[$loclsubs[$key]]} = $sub;
2326 $tmp = &$$tag;
2327 $tmp[hexdec($key)] = hexdec($sub);
2331 } else if (!isset($v['context'])) {
2332 $key = $v['match'];
2333 $key = preg_replace('/[\(\)]*/', '', $key);
2334 $sub = $v['replace'];
2335 if ($key && strlen(trim($key)) == 11 && $sub) {
2336 // If Cons Halant <rphf> and <half> always
2337 // and <pref>, <blwf> and <pstf> in Old version
2338 // If Halant Cons <pref>, <blwf> and <pstf> in New version only
2339 if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', substr($key, 0, 5)) !== false && strpos('pref blwf pstf', $v['tag']) !== false && !$is_old_spec) {
2340 $key = substr($key, 6, 5);
2341 $tag = $v['tag'];
2342 if (isset($loclsubs[$key])) {
2343 ${$tag[$loclsubs[$key]]} = $sub;
2345 $tmp = &$$tag;
2346 $tmp[hexdec($key)] = hexdec($sub);
2347 } else if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', substr($key, 6, 5)) !== false && (strpos('rphf half', $v['tag']) !== false || (strpos('pref blwf pstf', $v['tag']) !== false && ($is_old_spec || _OTL_OLD_SPEC_COMPAT_2)))) {
2348 $key = substr($key, 0, 5);
2349 $tag = $v['tag'];
2350 if (isset($loclsubs[$key])) {
2351 ${$tag[$loclsubs[$key]]} = $sub;
2353 $tmp = &$$tag;
2354 $tmp[hexdec($key)] = hexdec($sub);
2361 print_r($rphf );
2362 print_r($half );
2363 print_r($pref );
2364 print_r($blwf );
2365 print_r($pstf ); exit;
2368 //print_r($rtlpua); exit;
2369 //=====================================================================================
2370 //=====================================================================================
2371 //=====================================================================================
2372 //=====================================================================================
2373 if (count($rtl) || count($rphf) || count($half) || count($pref) || count($blwf) || count($pstf) || $finals) {
2374 // SAVE LOOKUPS TO FILE fontname.GSUB.scripttag.langtag.php
2376 $s = '<?php
2378 $rtlSUB = ' . $rtlSUB . ';
2379 $finals = \'' . $finals . '\';
2380 $rphf = ' . var_export($rphf, true) . ';
2381 $half = ' . var_export($half, true) . ';
2382 $pref = ' . var_export($pref, true) . ';
2383 $blwf = ' . var_export($blwf, true) . ';
2384 $pstf = ' . var_export($pstf, true) . ';
2386 ' . "\n" . '?>';
2388 $this->fontCache->write($this->fontkey . '.GSUB.' . $st . '.' . $t . '.php', $s);
2390 //=====================================================================================
2391 if (!isset($GSUBScriptLang[$st])) {
2392 $GSUBScriptLang[$st] = '';
2394 $GSUBScriptLang[$st] .= $t . ' ';
2395 //=====================================================================================
2398 //print_r($rtlpua); exit;
2399 // All RTL glyphs from font added to (or already in) PUA [reqd for magic_reverse]
2400 $rtlPUAstr = "";
2401 if (count($rtlpua)) {
2402 $rtlpua = array_unique($rtlpua);
2403 sort($rtlpua);
2404 $n = count($rtlpua);
2405 for ($i = 0; $i < $n; $i++) {
2406 if (hexdec($rtlpua[$i]) < hexdec('E000') || hexdec($rtlpua[$i]) > hexdec('F8FF')) {
2407 unset($rtlpua[$i]);
2410 sort($rtlpua, SORT_STRING);
2412 $rangeid = -1;
2413 $range = [];
2414 $prevgid = -2;
2415 // for each character
2416 foreach ($rtlpua as $gidhex) {
2417 $gid = hexdec($gidhex);
2418 if ($gid == ($prevgid + 1)) {
2419 $range[$rangeid]['end'] = $gidhex;
2420 $range[$rangeid]['count']++;
2421 } else {
2422 // new range
2423 $rangeid++;
2424 $range[$rangeid] = [];
2425 $range[$rangeid]['start'] = $gidhex;
2426 $range[$rangeid]['end'] = $gidhex;
2427 $range[$rangeid]['count'] = 1;
2429 $prevgid = $gid;
2431 foreach ($range as $rg) {
2432 if ($rg['count'] == 1) {
2433 $rtlPUAstr .= "\x{" . $rg['start'] . "}";
2434 } else if ($rg['count'] == 2) {
2435 $rtlPUAstr .= "\x{" . $rg['start'] . "}\x{" . $rg['end'] . "}";
2436 } else {
2437 $rtlPUAstr .= "\x{" . $rg['start'] . "}-\x{" . $rg['end'] . "}";
2442 //print_r($rtlPUAstr ); exit;
2443 //=====================================================================================
2444 //=====================================================================================
2445 //=====================================================================================
2446 //=====================================================================================
2447 //print_r($rtlpua); exit;
2448 //print_r($GSUBScriptLang); exit;
2451 //print_r($Lookup); exit;
2453 return [$GSUBScriptLang, $gsub, $GSLookup, $rtlPUAstr]; // , $rtlPUAarr Not needed
2456 /////////////////////////////////////////////////////////////////////////////////////////
2457 // GSUB functions
2458 function _getGSUBarray(&$Lookup, &$lul, $scripttag)
2460 // Process (3) LookupList for specific Script-LangSys
2461 // Generate preg_replace
2462 $volt = [];
2463 $reph = '';
2464 $matraE = '';
2465 $vatu = '';
2466 foreach ($lul as $i => $tag) {
2467 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
2468 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format'];
2470 // LookupType 1: Single Substitution Subtable
2471 if ($Lookup[$i]['Type'] == 1) {
2472 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2473 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2474 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2475 // Ignore has already been applied earlier on
2476 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()");
2477 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0);
2478 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 1];
2480 } // LookupType 2: Multiple Substitution Subtable
2481 else if ($Lookup[$i]['Type'] == 2) {
2482 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2483 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2484 $substitute = implode(" ", $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']);
2485 // Ignore has already been applied earlier on
2486 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()");
2487 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0);
2488 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 2];
2490 } // LookupType 3: Alternate Forms
2491 else if ($Lookup[$i]['Type'] == 3) {
2492 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2493 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2494 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2495 // Ignore has already been applied earlier on
2496 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()");
2497 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0);
2498 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 3];
2500 } // LookupType 4: Ligature Substitution Subtable
2501 else if ($Lookup[$i]['Type'] == 4) {
2502 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2503 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2504 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2505 // Ignore has already been applied earlier on
2506 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2507 $repl = $this->_makeGSUBinputMatch($inputGlyphs, $ignore);
2508 $subs = $this->_makeGSUBinputReplacement(count($inputGlyphs), $substitute, $ignore, 0, count($inputGlyphs), 0);
2509 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 4, 'CompCount' => $Lookup[$i]['Subtable'][$c]['subs'][$s]['CompCount'], 'Lig' => $substitute];
2511 } // LookupType 5: Chaining Contextual Substitution Subtable
2512 else if ($Lookup[$i]['Type'] == 5) {
2513 // Format 1: Context Substitution
2514 if ($SubstFormat == 1) {
2515 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2516 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) {
2517 // SubRuleSet
2518 $subRule = [];
2519 foreach ($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'] as $rule) {
2520 // SubRule
2521 $inputGlyphs = [];
2522 if ($rule['GlyphCount'] > 1) {
2523 $inputGlyphs = $rule['InputGlyphs'];
2525 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'];
2526 ksort($inputGlyphs);
2527 $nInput = count($inputGlyphs);
2529 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2530 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],];
2532 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2533 $lup = $rule['SubstLookupRecord'][$b]['LookupListIndex'];
2534 $seqIndex = $rule['SubstLookupRecord'][$b]['SequenceIndex'];
2536 // $Lookup[$lup] = secondary Lookup
2537 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2538 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2539 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2540 $lookupGlyphs = $luss['Replace'];
2541 $mLen = count($lookupGlyphs);
2543 // Only apply if the (first) 'Replace' glyph from the
2544 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2545 // then apply the substitution
2546 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2547 continue;
2549 $REPL = implode(" ", $luss['substitute']);
2550 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2551 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2552 } else {
2553 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2560 if (count($subRule['rules'])) {
2561 $volt[] = $subRule;
2565 } // Format 2: Class-based Context Glyph Substitution
2566 else if ($SubstFormat == 2) {
2567 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2568 foreach ($Lookup[$i]['Subtable'][$c]['SubClassSet'] as $inputClass => $cscs) {
2569 for ($cscrule = 0; $cscrule < $cscs['SubClassRuleCnt']; $cscrule++) {
2570 $rule = $cscs['SubClassRule'][$cscrule];
2572 $inputGlyphs = [];
2574 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass];
2575 if ($rule['InputGlyphCount'] > 1) {
2576 // NB starts at 1
2577 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) {
2578 $classindex = $rule['Input'][$gcl];
2579 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) {
2580 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex];
2581 } // if class[0] = all glyphs excluding those specified in all other classes
2582 // set to blank '' for now
2583 else {
2584 $inputGlyphs[$gcl] = '';
2589 $nInput = $rule['InputGlyphCount'];
2591 $nIsubs = (2 * $nInput) - 1;
2593 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2594 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],];
2596 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2597 $lup = $rule['LookupListIndex'][$b];
2598 $seqIndex = $rule['SequenceIndex'][$b];
2600 // $Lookup[$lup] = secondary Lookup
2601 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2602 if (isset($Lookup[$lup]['Subtable'][$lus]['subs']) && count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2603 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2604 $lookupGlyphs = $luss['Replace'];
2605 $mLen = count($lookupGlyphs);
2607 // Only apply if the (first) 'Replace' glyph from the
2608 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2609 // then apply the substitution
2610 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2611 continue;
2614 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2615 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex);
2616 $REPL = implode(" ", $luss['substitute']);
2617 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}"
2619 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2620 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2621 } else {
2622 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2628 if (count($subRule['rules'])) {
2629 $volt[] = $subRule;
2633 } // Format 3: Coverage-based Context Glyph Substitution p259
2634 else if ($SubstFormat == 3) {
2635 // IgnoreMarks flag set on main Lookup table
2636 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2637 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'];
2638 $CoverageInputGlyphs = implode('|', $inputGlyphs);
2639 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount'];
2641 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) {
2642 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'];
2643 } else {
2644 $backtrackGlyphs = [];
2646 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦
2647 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore);
2649 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) {
2650 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'];
2651 } else {
2652 $lookaheadGlyphs = [];
2654 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦
2655 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore);
2657 $nBsubs = 2 * count($backtrackGlyphs);
2658 $nIsubs = (2 * $nInput) - 1;
2659 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2660 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],];
2662 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
2663 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'];
2664 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'];
2665 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2666 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2667 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2668 $lookupGlyphs = $luss['Replace'];
2669 $mLen = count($lookupGlyphs);
2671 // Only apply if the (first) 'Replace' glyph from the
2672 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2673 // then apply the substitution
2674 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2675 continue;
2678 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2679 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex);
2680 $REPL = implode(" ", $luss['substitute']);
2682 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2683 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2684 } else {
2685 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2691 if (count($subRule['rules'])) {
2692 $volt[] = $subRule;
2696 //print_r($Lookup[$i]);
2697 //print_r($volt[(count($volt)-1)]); exit;
2698 } // LookupType 6: ing Contextual Substitution Subtable
2699 else if ($Lookup[$i]['Type'] == 6) {
2700 // Format 1: Simple Chaining Context Glyph Substitution p255
2701 if ($SubstFormat == 1) {
2702 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2703 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $s++) {
2704 // ChainSubRuleSet
2705 $subRule = [];
2706 $firstInputGlyph = $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'][$s]; // First input gyyph
2707 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'] as $rule) {
2708 // ChainSubRule
2709 $inputGlyphs = [];
2710 if ($rule['InputGlyphCount'] > 1) {
2711 $inputGlyphs = $rule['InputGlyphs'];
2713 $inputGlyphs[0] = $firstInputGlyph;
2714 ksort($inputGlyphs);
2715 $nInput = count($inputGlyphs);
2717 if ($rule['BacktrackGlyphCount']) {
2718 $backtrackGlyphs = $rule['BacktrackGlyphs'];
2719 } else {
2720 $backtrackGlyphs = [];
2722 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore);
2724 if ($rule['LookaheadGlyphCount']) {
2725 $lookaheadGlyphs = $rule['LookaheadGlyphs'];
2726 } else {
2727 $lookaheadGlyphs = [];
2730 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore);
2732 $nBsubs = 2 * count($backtrackGlyphs);
2733 $nIsubs = (2 * $nInput) - 1;
2735 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2736 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],];
2738 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2739 $lup = $rule['LookupListIndex'][$b];
2740 $seqIndex = $rule['SequenceIndex'][$b];
2742 // $Lookup[$lup] = secondary Lookup
2743 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2744 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2745 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2746 $lookupGlyphs = $luss['Replace'];
2747 $mLen = count($lookupGlyphs);
2749 // Only apply if the (first) 'Replace' glyph from the
2750 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2751 // then apply the substitution
2752 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2753 continue;
2756 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2757 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex);
2759 $REPL = implode(" ", $luss['substitute']);
2761 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2762 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2763 } else {
2764 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2771 if (count($subRule['rules'])) {
2772 $volt[] = $subRule;
2776 } // Format 2: Class-based Chaining Context Glyph Substitution p257
2777 else if ($SubstFormat == 2) {
2778 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2779 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'] as $inputClass => $cscs) {
2780 for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) {
2781 $rule = $cscs['ChainSubClassRule'][$cscrule];
2783 // These contain classes of glyphs as strings
2784 // $Lookup[$i]['Subtable'][$c]['InputClasses'][(class)] e.g. 02E6|02E7|02E8
2785 // $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][(class)]
2786 // $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][(class)]
2787 // These contain arrays of classIndexes
2788 // [Backtrack] [Lookahead] and [Input] (Input is from the second position only)
2790 $inputGlyphs = [];
2792 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass])) {
2793 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass];
2794 } else {
2795 $inputGlyphs[0] = '';
2797 if ($rule['InputGlyphCount'] > 1) {
2798 // NB starts at 1
2799 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) {
2800 $classindex = $rule['Input'][$gcl];
2801 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) {
2802 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex];
2803 } // if class[0] = all glyphs excluding those specified in all other classes
2804 // set to blank '' for now
2805 else {
2806 $inputGlyphs[$gcl] = '';
2811 $nInput = $rule['InputGlyphCount'];
2813 if ($rule['BacktrackGlyphCount']) {
2814 for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
2815 $classindex = $rule['Backtrack'][$gcl];
2816 if (isset($Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex])) {
2817 $backtrackGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex];
2818 } // if class[0] = all glyphs excluding those specified in all other classes
2819 // set to blank '' for now
2820 else {
2821 $backtrackGlyphs[$gcl] = '';
2824 } else {
2825 $backtrackGlyphs = [];
2827 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦
2828 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore);
2830 if ($rule['LookaheadGlyphCount']) {
2831 for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
2832 $classindex = $rule['Lookahead'][$gcl];
2833 if (isset($Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex])) {
2834 $lookaheadGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex];
2835 } // if class[0] = all glyphs excluding those specified in all other classes
2836 // set to blank '' for now
2837 else {
2838 $lookaheadGlyphs[$gcl] = '';
2841 } else {
2842 $lookaheadGlyphs = [];
2844 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦
2845 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore);
2847 $nBsubs = 2 * count($backtrackGlyphs);
2848 $nIsubs = (2 * $nInput) - 1;
2850 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2851 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],];
2853 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2854 $lup = $rule['LookupListIndex'][$b];
2855 $seqIndex = $rule['SequenceIndex'][$b];
2857 // $Lookup[$lup] = secondary Lookup
2858 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2859 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2860 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2861 $lookupGlyphs = $luss['Replace'];
2862 $mLen = count($lookupGlyphs);
2864 // Only apply if the (first) 'Replace' glyph from the
2865 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2866 // then apply the substitution
2867 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2868 continue;
2871 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2872 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex);
2873 $REPL = implode(" ", $luss['substitute']);
2874 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}"
2876 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2877 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2878 } else {
2879 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2885 if (count($subRule['rules'])) {
2886 $volt[] = $subRule;
2891 //print_r($Lookup[$i]['Subtable'][$c]); exit;
2892 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
2893 else if ($SubstFormat == 3) {
2894 // IgnoreMarks flag set on main Lookup table
2895 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
2896 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'];
2897 $CoverageInputGlyphs = implode('|', $inputGlyphs);
2898 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount'];
2900 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) {
2901 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'];
2902 } else {
2903 $backtrackGlyphs = [];
2905 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦
2906 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore);
2908 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) {
2909 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'];
2910 } else {
2911 $lookaheadGlyphs = [];
2913 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦
2914 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore);
2916 $nBsubs = 2 * count($backtrackGlyphs);
2917 $nIsubs = (2 * $nInput) - 1;
2918 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0);
2919 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],];
2921 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
2922 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'];
2923 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'];
2924 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) {
2925 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) {
2926 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) {
2927 $lookupGlyphs = $luss['Replace'];
2928 $mLen = count($lookupGlyphs);
2930 // Only apply if the (first) 'Replace' glyph from the
2931 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2932 // then apply the substitution
2933 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) {
2934 continue;
2937 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2938 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex);
2939 $REPL = implode(" ", $luss['substitute']);
2941 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') {
2942 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];
2943 } else {
2944 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],];
2950 if (count($subRule['rules'])) {
2951 $volt[] = $subRule;
2958 //print_r($Lookup); exit;
2959 return $volt;
2962 //=====================================================================================
2963 //=====================================================================================
2964 // mPDF 5.7.1
2965 function _checkGSUBignore($flag, $glyph, $MarkFilteringSet)
2967 $ignore = false;
2968 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
2969 if ((($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks, $glyph)) {
2970 $ignore = true;
2972 if ((($flag & 0x0004) == 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) {
2973 $ignore = true;
2975 if ((($flag & 0x0002) == 0x0002) && strpos($this->GlyphClassBases, $glyph)) {
2976 $ignore = true;
2978 // Flag & 0xFF?? = MarkAttachmentType
2979 if ($flag & 0xFF00) {
2980 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
2981 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
2982 if (strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) {
2983 $ignore = true;
2986 // Flag & 0x0010 = UseMarkFilteringSet
2987 if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) {
2988 $ignore = true;
2991 return $ignore;
2994 function _getGSUBignoreString($flag, $MarkFilteringSet)
2996 // If ignoreFlag set, combine all ignore glyphs into -> "((?:(?: FBA1| FBA2| FBA3))*)"
2997 // else "()"
2998 // for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup
2999 $str = "";
3000 $ignoreflag = 0;
3002 // Flag & 0xFF?? = MarkAttachmentType
3003 if ($flag & 0xFF00) {
3004 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
3005 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
3006 $MarkAttachmentType = $flag >> 8;
3007 $ignoreflag = $flag;
3008 $str = $this->MarkAttachmentType[$MarkAttachmentType];
3011 // Flag & 0x0010 = UseMarkFilteringSet
3012 if ($flag & 0x0010) {
3013 throw new \Mpdf\MpdfException("This font " . $this->fontkey . " contains MarkGlyphSets - Not tested yet");
3014 $str = $this->MarkGlyphSets[$MarkFilteringSet];
3017 // If Ignore Marks set, supercedes any above
3018 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
3019 if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {
3020 $ignoreflag = 8;
3021 $str = $this->GlyphClassMarks;
3024 // Flag & 0x0004 = Ignore Ligatures
3025 if (($flag & 0x0004) == 0x0004) {
3026 $ignoreflag += 4;
3027 if ($str) {
3028 $str .= "|";
3030 $str .= $this->GlyphClassLigatures;
3032 // Flag & 0x0002 = Ignore BaseGlyphs
3033 if (($flag & 0x0002) == 0x0002) {
3034 $ignoreflag += 2;
3035 if ($str) {
3036 $str .= "|";
3038 $str .= $this->GlyphClassBases;
3040 if ($str) {
3041 // This originally returned e.g. ((?:(?:[IGNORE8]))*) when NOT specific to a Lookup e.g. rtlSub in
3042 // arabictypesetting.GSUB.arab.DFLT.php
3043 // This would save repeatedly saving long text strings if used multiple times
3044 // When writing e.g. arabictypesetting.GSUB.arab.DFLT.php to file, included as $ignore[8]
3045 // Would need to also write the $ignore array to that file
3046 // // If UseMarkFilteringSet (specific to the Lookup) return the string
3047 // if (($flag & 0x0010) && ($flag & 0x0008) != 0x0008) {
3048 // return "((?:(?:" . $str . "))*)";
3049 // }
3050 // else { return "((?:(?:" . "[IGNORE".$ignoreflag."]" . "))*)"; }
3051 // // e.g. ((?:(?: 0031| 0032| 0033| 0034| 0045))*)
3052 // But never finished coding it to add the $ignore array to the file, and it doesn't seem to occur often enough to be worth
3053 // writing. So just output it as a string:
3054 return "((?:(?:" . $str . "))*)";
3055 } else {
3056 return "()";
3060 // GSUB Patterns
3063 BACKTRACK INPUT LOOKAHEAD
3064 ================================== ================== ==================================
3065 (FEEB|FEEC)(ign) ¦(FD12|FD13)(ign) ¦(0612)¦(ign) (0613)¦(ign) (FD12|FD13)¦(ign) (FEEB|FEEC)
3066 ---------------- ---------------- ----- ------------ --------------- ---------------
3067 Backtrack 1 Backtrack 2 Input 1 Input 2 Lookahead 1 Lookahead 2
3068 -------- --- --------- --- ---- --- ---- --- --------- --- -------
3069 \${1} \${2} \${3} \${4} \${5+} \${6+} \${7+} \${8+}
3071 nBacktrack = 2 nInput = 2 nLookahead = 2
3073 nBsubs = 2xnBack nIsubs = (nBsubs+) nLsubs = (nBsubs+nIsubs+) 2xnLookahead
3074 "\${1}\${2} " (nInput*2)-1 "\${5+} \${6+}"
3075 "REPL"
3077 ¦\${1}\${2} ¦\${3}\${4} ¦REPL¦\${5+} \${6+}¦\${7+} \${8+}¦
3080 INPUT nInput = 5
3081 ============================================================
3082 ¦(0612)¦(ign) (0613)¦(ign) (0614)¦(ign) (0615)¦(ign) (0615)¦
3083 \${1} \${2} \${3} \${4} \${5} \${6} \${7} \${8} \${9} (All backreference numbers are + nBsubs)
3084 ----- ------------ ------------ ------------ ------------
3085 Input 1 Input 2 Input 3 Input 4 Input 5
3087 A====== SequenceIndex=1 ; Lookup match nGlyphs=1
3088 B=================== SequenceIndex=1 ; Lookup match nGlyphs=2
3089 C=============================== SequenceIndex=1 ; Lookup match nGlyphs=3
3090 D======================= SequenceIndex=2 ; Lookup match nGlyphs=2
3091 E===================================== SequenceIndex=2 ; Lookup match nGlyphs=3
3092 F====================== SequenceIndex=4 ; Lookup match nGlyphs=2
3094 All backreference numbers are + nBsubs
3095 A - "REPL\${2} \${3}\${4} \${5}\${6} \${7}\${8} \${9}"
3096 B - "REPL\${2}\${4} \${5}\${6} \${7}\${8} \${9}"
3097 C - "REPL\${2}\${4}\${6} \${7}\${8} \${9}"
3098 D - "\${1} REPL\${2}\${4}\${6} \${7}\${8} \${9}"
3099 E - "\${1} REPL\${2}\${4}\${6}\${8} \${9}"
3100 F - "\${1}\${2} \${3}\${4} \${5} REPL\${6}\${8}"
3103 function _makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex)
3105 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
3106 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
3107 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context
3108 // $lookupGlyphs = array of glyphs (single Glyphs) making up Lookup Input sequence
3109 $mLen = count($lookupGlyphs); // nGlyphs in the secondary Lookup match
3110 $nInput = count($inputGlyphs); // nGlyphs in the Primary Input sequence
3111 $str = "";
3112 for ($i = 0; $i < $nInput; $i++) {
3113 if ($i > 0) {
3114 $str .= $ignore . " ";
3116 if ($i >= $seqIndex && $i < ($seqIndex + $mLen)) {
3117 $str .= "(" . $lookupGlyphs[($i - $seqIndex)] . ")";
3118 } else {
3119 $str .= "(" . $inputGlyphs[($i)] . ")";
3123 return $str;
3126 function _makeGSUBinputMatch($inputGlyphs, $ignore)
3128 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
3129 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
3130 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context
3131 // $lookupGlyphs = array of glyphs making up Lookup Input sequence - if applicable
3132 $str = "";
3133 for ($i = 1; $i <= count($inputGlyphs); $i++) {
3134 if ($i > 1) {
3135 $str .= $ignore . " ";
3137 $str .= "(" . $inputGlyphs[($i - 1)] . ")";
3140 return $str;
3143 function _makeGSUBbacktrackMatch($backtrackGlyphs, $ignore)
3145 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
3146 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦
3147 // $backtrackGlyphs = array of glyphstrings making up Backtrack sequence
3148 // 3 2 1 0
3149 // each item being e.g. E0AD|E0AF|F1FD
3150 $str = "";
3151 for ($i = (count($backtrackGlyphs) - 1); $i >= 0; $i--) {
3152 $str .= "(" . $backtrackGlyphs[$i] . ")" . $ignore . " ";
3155 return $str;
3158 function _makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore)
3160 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
3161 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦
3162 // $lookaheadGlyphs = array of glyphstrings making up Lookahead sequence
3163 // 0 1 2 3
3164 // each item being e.g. E0AD|E0AF|F1FD
3165 $str = "";
3166 for ($i = 0; $i < count($lookaheadGlyphs); $i++) {
3167 $str .= $ignore . " (" . $lookaheadGlyphs[$i] . ")";
3170 return $str;
3173 function _makeGSUBinputReplacement($nInput, $REPL, $ignore, $nBsubs, $mLen, $seqIndex)
3175 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}"
3176 // $nInput nGlyphs in the Primary Input sequence
3177 // $REPL replacement glyphs from secondary lookup
3178 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
3179 // $nBsubs Number of Backtrack substitutions (= 2x Number of Backtrack glyphs)
3180 // $mLen nGlyphs in the secondary Lookup match - if no secondary lookup, should=$nInput
3181 // $seqIndex Sequence Index to apply the secondary match
3182 if ($ignore == "()") {
3183 $ign = false;
3184 } else {
3185 $ign = true;
3187 $str = "";
3188 if ($nInput == 1) {
3189 $str = $REPL;
3190 } else if ($nInput > 1) {
3191 if ($mLen == $nInput) { // whole string replaced
3192 $str = $REPL;
3193 if ($ign) {
3194 // for every nInput over 1, add another replacement backreference, to move IGNORES after replacement
3195 for ($x = 2; $x <= $nInput; $x++) {
3196 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
3199 } else { // if only part of string replaced:
3200 for ($x = 1; $x < ($seqIndex + 1); $x++) {
3201 if ($x == 1) {
3202 $str .= '\\' . ($nBsubs + 1);
3203 } else {
3204 if ($ign) {
3205 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
3207 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1)));
3210 if ($seqIndex > 0) {
3211 $str .= " ";
3213 $str .= $REPL;
3214 if ($ign) {
3215 for ($x = (max(($seqIndex + 1), 2)); $x < ($seqIndex + 1 + $mLen); $x++) { // move IGNORES after replacement
3216 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
3219 for ($x = ($seqIndex + 1 + $mLen); $x <= $nInput; $x++) {
3220 if ($ign) {
3221 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
3223 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1)));
3228 return $str;
3231 //////////////////////////////////////////////////////////////////////////////////
3232 function _getCoverage($convert2hex = true, $mode = 1)
3234 $g = [];
3235 $ctr = 0;
3236 $CoverageFormat = $this->read_ushort();
3237 if ($CoverageFormat == 1) {
3238 $CoverageGlyphCount = $this->read_ushort();
3239 for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
3240 $glyphID = $this->read_ushort();
3241 $uni = $this->glyphToChar[$glyphID][0];
3242 if ($convert2hex) {
3243 $g[] = unicode_hex($uni);
3244 } else if ($mode == 2) {
3245 $g[$uni] = $ctr;
3246 $ctr++;
3247 } else {
3248 $g[] = $glyphID;
3252 if ($CoverageFormat == 2) {
3253 $RangeCount = $this->read_ushort();
3254 for ($r = 0; $r < $RangeCount; $r++) {
3255 $start = $this->read_ushort();
3256 $end = $this->read_ushort();
3257 $StartCoverageIndex = $this->read_ushort(); // n/a
3258 for ($glyphID = $start; $glyphID <= $end; $glyphID++) {
3259 $uni = $this->glyphToChar[$glyphID][0];
3260 if ($convert2hex) {
3261 $g[] = unicode_hex($uni);
3262 } else if ($mode == 2) {
3263 $uni = $g[$uni] = $ctr;
3264 $ctr++;
3265 } else {
3266 $g[] = $glyphID;
3272 return $g;
3275 //////////////////////////////////////////////////////////////////////////////////
3276 function _getClasses($offset)
3278 $this->seek($offset);
3279 $ClassFormat = $this->read_ushort();
3280 $GlyphByClass = [];
3281 if ($ClassFormat == 1) {
3282 $StartGlyph = $this->read_ushort();
3283 $GlyphCount = $this->read_ushort();
3284 for ($i = 0; $i < $GlyphCount; $i++) {
3285 $startGlyphID = $StartGlyph + $i;
3286 $endGlyphID = $StartGlyph + $i;
3287 $class = $this->read_ushort();
3288 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
3289 if (isset($this->glyphToChar[$g][0])) {
3290 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]);
3294 } else if ($ClassFormat == 2) {
3295 $tableCount = $this->read_ushort();
3296 for ($i = 0; $i < $tableCount; $i++) {
3297 $startGlyphID = $this->read_ushort();
3298 $endGlyphID = $this->read_ushort();
3299 $class = $this->read_ushort();
3300 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
3301 if ($this->glyphToChar[$g][0]) {
3302 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]);
3307 $gbc = [];
3308 foreach ($GlyphByClass as $class => $garr) {
3309 $gbc[$class] = implode('|', $garr);
3312 return $gbc;
3315 //////////////////////////////////////////////////////////////////////////////////
3316 //////////////////////////////////////////////////////////////////////////////////
3317 //////////////////////////////////////////////////////////////////////////////////
3318 //////////////////////////////////////////////////////////////////////////////////
3319 //////////////////////////////////////////////////////////////////////////////////
3320 function _getGPOStables()
3322 ///////////////////////////////////
3323 // GPOS - Glyph Positioning
3324 ///////////////////////////////////
3325 if (isset($this->tables["GPOS"])) {
3326 $ffeats = [];
3327 $gpos_offset = $this->seek_table("GPOS");
3328 $this->skip(4);
3329 $ScriptList_offset = $gpos_offset + $this->read_ushort();
3330 $FeatureList_offset = $gpos_offset + $this->read_ushort();
3331 $LookupList_offset = $gpos_offset + $this->read_ushort();
3333 // ScriptList
3334 $this->seek($ScriptList_offset);
3335 $ScriptCount = $this->read_ushort();
3336 for ($i = 0; $i < $ScriptCount; $i++) {
3337 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc.
3338 $ScriptTableOffset = $this->read_ushort();
3339 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset;
3342 // Script Table
3343 foreach ($ffeats as $t => $o) {
3344 $ls = [];
3345 $this->seek($o);
3346 $DefLangSys_offset = $this->read_ushort();
3347 if ($DefLangSys_offset > 0) {
3348 $ls['DFLT'] = $DefLangSys_offset + $o;
3350 $LangSysCount = $this->read_ushort();
3351 for ($i = 0; $i < $LangSysCount; $i++) {
3352 $LangTag = $this->read_tag(); // =
3353 $LangTableOffset = $this->read_ushort();
3354 $ls[$LangTag] = $o + $LangTableOffset;
3356 $ffeats[$t] = $ls;
3359 // Get FeatureIndexList
3360 // LangSys Table - from first listed langsys
3361 foreach ($ffeats as $st => $scripts) {
3362 foreach ($scripts as $t => $o) {
3363 $FeatureIndex = [];
3364 $langsystable_offset = $o;
3365 $this->seek($langsystable_offset);
3366 $LookUpOrder = $this->read_ushort(); //==NULL
3367 $ReqFeatureIndex = $this->read_ushort();
3368 if ($ReqFeatureIndex != 0xFFFF) {
3369 $FeatureIndex[] = $ReqFeatureIndex;
3371 $FeatureCount = $this->read_ushort();
3372 for ($i = 0; $i < $FeatureCount; $i++) {
3373 $FeatureIndex[] = $this->read_ushort(); // = index of feature
3375 $ffeats[$st][$t] = $FeatureIndex;
3378 //print_r($ffeats); exit;
3379 // Feauture List => LookupListIndex es
3380 $this->seek($FeatureList_offset);
3381 $FeatureCount = $this->read_ushort();
3382 $Feature = [];
3383 for ($i = 0; $i < $FeatureCount; $i++) {
3384 $tag = $this->read_tag();
3385 if ($tag == 'kern') {
3386 $this->haskernGPOS = true;
3388 $Feature[$i] = ['tag' => $tag];
3389 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort();
3391 for ($i = 0; $i < $FeatureCount; $i++) {
3392 $this->seek($Feature[$i]['offset']);
3393 $this->read_ushort(); // null
3394 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort();
3395 $Feature[$i]['LookupListIndex'] = [];
3396 for ($c = 0; $c < $Lookupcount; $c++) {
3397 $Feature[$i]['LookupListIndex'][] = $this->read_ushort();
3401 foreach ($ffeats as $st => $scripts) {
3402 foreach ($scripts as $t => $o) {
3403 $FeatureIndex = $ffeats[$st][$t];
3404 foreach ($FeatureIndex as $k => $fi) {
3405 $ffeats[$st][$t][$k] = $Feature[$fi];
3409 //print_r($ffeats); exit;
3410 //=====================================================================================
3411 $gpos = [];
3412 $GPOSScriptLang = [];
3413 foreach ($ffeats as $st => $scripts) {
3414 foreach ($scripts as $t => $langsys) {
3415 $lg = [];
3416 foreach ($langsys as $ft) {
3417 $lg[$ft['LookupListIndex'][0]] = $ft;
3419 // list of Lookups in order they need to be run i.e. order listed in Lookup table
3420 ksort($lg);
3421 foreach ($lg as $ft) {
3422 $gpos[$st][$t][$ft['tag']] = $ft['LookupListIndex'];
3424 if (!isset($GPOSScriptLang[$st])) {
3425 $GPOSScriptLang[$st] = '';
3427 $GPOSScriptLang[$st] .= $t . ' ';
3431 //=====================================================================================
3432 // Get metadata and offsets for whole Lookup List table
3433 $this->seek($LookupList_offset);
3434 $LookupCount = $this->read_ushort();
3435 $Lookup = [];
3436 $Offsets = [];
3437 $SubtableCount = [];
3438 for ($i = 0; $i < $LookupCount; $i++) {
3439 $Offsets[$i] = $LookupList_offset + $this->read_ushort();
3441 for ($i = 0; $i < $LookupCount; $i++) {
3442 $this->seek($Offsets[$i]);
3443 $Lookup[$i]['Type'] = $this->read_ushort();
3444 $Lookup[$i]['Flag'] = $flag = $this->read_ushort();
3445 $Lookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort();
3446 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
3447 $Lookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort();
3449 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
3450 if (($flag & 0x0010) == 0x0010) {
3451 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort();
3452 } else {
3453 $Lookup[$i]['MarkFilteringSet'] = '';
3456 // Lookup Type 9: Extension
3457 if ($Lookup[$i]['Type'] == 9) {
3458 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
3459 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
3460 $this->seek($Lookup[$i]['Subtables'][$c]);
3461 $ExtensionPosFormat = $this->read_ushort();
3462 $type = $this->read_ushort();
3463 $Lookup[$i]['Subtables'][$c] = $Lookup[$i]['Subtables'][$c] + $this->read_ulong();
3465 $Lookup[$i]['Type'] = $type;
3469 //=====================================================================================
3470 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph
3471 $this->LuCoverage = [];
3472 for ($i = 0; $i < $LookupCount; $i++) {
3473 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
3474 $this->seek($Lookup[$i]['Subtables'][$c]);
3475 $PosFormat = $this->read_ushort();
3477 if ($Lookup[$i]['Type'] == 7 && $PosFormat == 3) {
3478 $this->skip(4);
3479 } else if ($Lookup[$i]['Type'] == 8 && $PosFormat == 3) {
3480 $BacktrackGlyphCount = $this->read_ushort();
3481 $this->skip(2 * $BacktrackGlyphCount + 2);
3483 // NB Coverage only looks at glyphs for position 1 (i.e. 7.3 and 8.3) // NEEDS TO READ ALL ********************
3484 // NB For e.g. Type 4, this may be the Coverage for the Mark
3485 $Coverage = $Lookup[$i]['Subtables'][$c] + $this->read_ushort();
3486 $this->seek($Coverage);
3487 $glyphs = $this->_getCoverage(false, 2);
3488 $this->LuCoverage[$i][$c] = $glyphs;
3492 //=====================================================================================
3493 //print_r($GPOSScriptLang); exit;
3494 //print_r($gpos); exit;
3495 //print_r($Lookup); exit;
3497 $s = '<?php
3498 $LuCoverage = ' . var_export($this->LuCoverage, true) . ';
3499 ?>';
3501 $this->fontCache->write($this->fontkey . '.GPOSdata.php', $s);
3503 return [$GPOSScriptLang, $gpos, $Lookup];
3504 } // end if GPOS
3507 //////////////////////////////////////////////////////////////////////////////////
3508 //=====================================================================================
3509 //=====================================================================================
3510 //=====================================================================================
3511 //=====================================================================================
3512 //=====================================================================================
3513 //=====================================================================================
3515 function makeSubset($file, &$subset, $TTCfontID = 0, $debug = false, $useOTL = false)
3517 // mPDF 5.7.1
3518 $this->useOTL = $useOTL; // mPDF 5.7.1
3519 $this->filename = $file;
3520 $this->fh = fopen($file, 'rb');
3522 if (!$this->fh) {
3523 throw new \Mpdf\MpdfException('Can\'t open file ' . $file);
3526 $this->_pos = 0;
3527 $this->charWidths = '';
3528 $this->glyphPos = [];
3529 $this->charToGlyph = [];
3530 $this->tables = [];
3531 $this->otables = [];
3532 $this->ascent = 0;
3533 $this->descent = 0;
3534 $this->strikeoutSize = 0;
3535 $this->strikeoutPosition = 0;
3536 $this->numTTCFonts = 0;
3537 $this->TTCFonts = [];
3538 $this->skip(4);
3539 $this->maxUni = 0;
3540 if ($TTCfontID > 0) {
3541 $this->version = $version = $this->read_ulong(); // TTC Header version now
3542 if (!in_array($version, [0x00010000, 0x00020000])) {
3543 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
3545 $this->numTTCFonts = $this->read_ulong();
3546 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
3547 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
3549 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
3550 $this->version = $version = $this->read_ulong(); // TTFont version again now
3552 $this->readTableDirectory($debug);
3554 ///////////////////////////////////
3555 // head - Font header table
3556 ///////////////////////////////////
3557 $this->seek_table("head");
3558 $this->skip(50);
3559 $indexToLocFormat = $this->read_ushort();
3560 $glyphDataFormat = $this->read_ushort();
3562 ///////////////////////////////////
3563 // hhea - Horizontal header table
3564 ///////////////////////////////////
3565 $this->seek_table("hhea");
3566 $this->skip(32);
3567 $metricDataFormat = $this->read_ushort();
3568 $orignHmetrics = $numberOfHMetrics = $this->read_ushort();
3570 ///////////////////////////////////
3571 // maxp - Maximum profile table
3572 ///////////////////////////////////
3573 $this->seek_table("maxp");
3574 $this->skip(4);
3575 $numGlyphs = $this->read_ushort();
3577 ///////////////////////////////////
3578 // cmap - Character to glyph index mapping table
3579 ///////////////////////////////////
3580 $cmap_offset = $this->seek_table("cmap");
3581 $this->skip(2);
3582 $cmapTableCount = $this->read_ushort();
3583 $unicode_cmap_offset = 0;
3584 for ($i = 0; $i < $cmapTableCount; $i++) {
3585 $platformID = $this->read_ushort();
3586 $encodingID = $this->read_ushort();
3587 $offset = $this->read_ulong();
3588 $save_pos = $this->_pos;
3589 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode
3590 $format = $this->get_ushort($cmap_offset + $offset);
3591 if ($format == 4) {
3592 $unicode_cmap_offset = $cmap_offset + $offset;
3593 break;
3596 $this->seek($save_pos);
3599 if (!$unicode_cmap_offset) {
3600 throw new \Mpdf\MpdfException('Font (' . $this->filename . ') does not have Unicode cmap (platform 3, encoding 1, format 4, or platform 0 [any encoding] format 4)');
3603 $glyphToChar = [];
3604 $charToGlyph = [];
3605 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
3607 ///////////////////////////////////
3608 // mPDF 5.7.1
3609 // Map Unmapped glyphs - from $numGlyphs
3610 if ($useOTL) {
3611 $bctr = 0xE000;
3612 for ($gid = 1; $gid < $numGlyphs; $gid++) {
3613 if (!isset($glyphToChar[$gid])) {
3614 while (isset($charToGlyph[$bctr])) {
3615 $bctr++;
3616 } // Avoid overwriting a glyph already mapped in PUA
3617 if ($bctr > 0xF8FF) {
3618 throw new \Mpdf\MpdfException($file . " : WARNING - Font cannot map all included glyphs into Private Use Area U+E000 - U+F8FF; cannot use useOTL on this font");
3620 $glyphToChar[$gid][] = $bctr;
3621 $charToGlyph[$bctr] = $gid;
3622 $bctr++;
3626 ///////////////////////////////////
3628 $this->charToGlyph = $charToGlyph;
3629 $this->glyphToChar = $glyphToChar;
3631 ///////////////////////////////////
3632 // hmtx - Horizontal metrics table
3633 ///////////////////////////////////
3634 $scale = 1; // not used
3635 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale);
3637 ///////////////////////////////////
3638 // loca - Index to location
3639 ///////////////////////////////////
3640 $this->getLOCA($indexToLocFormat, $numGlyphs);
3642 $subsetglyphs = [0 => 0, 1 => 1, 2 => 2];
3643 $subsetCharToGlyph = [];
3644 foreach ($subset as $code) {
3645 if (isset($this->charToGlyph[$code])) {
3646 $subsetglyphs[$this->charToGlyph[$code]] = $code; // Old Glyph ID => Unicode
3647 $subsetCharToGlyph[$code] = $this->charToGlyph[$code]; // Unicode to old GlyphID
3649 $this->maxUni = max($this->maxUni, $code);
3652 list($start, $dummy) = $this->get_table_pos('glyf');
3654 $glyphSet = [];
3655 ksort($subsetglyphs);
3656 $n = 0;
3657 $fsLastCharIndex = 0; // maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1.
3658 foreach ($subsetglyphs as $originalGlyphIdx => $uni) {
3659 $fsLastCharIndex = max($fsLastCharIndex, $uni);
3660 $glyphSet[$originalGlyphIdx] = $n; // old glyphID to new glyphID
3661 $n++;
3664 ksort($subsetCharToGlyph);
3665 foreach ($subsetCharToGlyph as $uni => $originalGlyphIdx) {
3666 $codeToGlyph[$uni] = $glyphSet[$originalGlyphIdx];
3668 $this->codeToGlyph = $codeToGlyph;
3670 ksort($subsetglyphs);
3671 foreach ($subsetglyphs as $originalGlyphIdx => $uni) {
3672 $this->getGlyphs($originalGlyphIdx, $start, $glyphSet, $subsetglyphs);
3675 $numGlyphs = $numberOfHMetrics = count($subsetglyphs);
3677 ///////////////////////////////////
3678 // name - table copied from the original
3679 ///////////////////////////////////
3680 // MS spec says that "Platform and encoding ID's in the name table should be consistent with those in the cmap table.
3681 // If they are not, the font will not load in Windows"
3682 // Doesn't seem to be a problem?
3683 ///////////////////////////////////
3684 $this->add('name', $this->get_table('name'));
3686 ///////////////////////////////////
3687 //tables copied from the original
3688 ///////////////////////////////////
3689 $tags = ['cvt ', 'fpgm', 'prep', 'gasp'];
3690 foreach ($tags as $tag) {
3691 if (isset($this->tables[$tag])) {
3692 $this->add($tag, $this->get_table($tag));
3696 ///////////////////////////////////
3697 // post - PostScript
3698 ///////////////////////////////////
3699 if (isset($this->tables['post'])) {
3700 $opost = $this->get_table('post');
3701 $post = "\x00\x03\x00\x00" . substr($opost, 4, 12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
3702 $this->add('post', $post);
3705 ///////////////////////////////////
3706 // Sort CID2GID map into segments of contiguous codes
3707 ///////////////////////////////////
3708 ksort($codeToGlyph);
3709 unset($codeToGlyph[0]);
3710 //unset($codeToGlyph[65535]);
3711 $rangeid = 0;
3712 $range = [];
3713 $prevcid = -2;
3714 $prevglidx = -1;
3715 // for each character
3716 foreach ($codeToGlyph as $cid => $glidx) {
3717 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) {
3718 $range[$rangeid][] = $glidx;
3719 } else {
3720 // new range
3721 $rangeid = $cid;
3722 $range[$rangeid] = [];
3723 $range[$rangeid][] = $glidx;
3725 $prevcid = $cid;
3726 $prevglidx = $glidx;
3729 ///////////////////////////////////
3730 // CMap table
3731 ///////////////////////////////////
3732 // cmap - Character to glyph mapping
3733 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF
3734 $searchRange = 1;
3735 $entrySelector = 0;
3736 while ($searchRange * 2 <= $segCount) {
3737 $searchRange = $searchRange * 2;
3738 $entrySelector = $entrySelector + 1;
3740 $searchRange = $searchRange * 2;
3741 $rangeShift = $segCount * 2 - $searchRange;
3742 $length = 16 + (8 * $segCount) + ($numGlyphs + 1);
3743 $cmap = [0, 3, // Index : version, number of encoding subtables
3744 0, 0, // Encoding Subtable : platform (UNI=0), encoding 0
3745 0, 28, // Encoding Subtable : offset (hi,lo)
3746 0, 3, // Encoding Subtable : platform (UNI=0), encoding 3
3747 0, 28, // Encoding Subtable : offset (hi,lo)
3748 3, 1, // Encoding Subtable : platform (MS=3), encoding 1
3749 0, 28, // Encoding Subtable : offset (hi,lo)
3750 4, $length, 0, // Format 4 Mapping subtable: format, length, language
3751 $segCount * 2,
3752 $searchRange,
3753 $entrySelector,
3754 $rangeShift];
3756 // endCode(s)
3757 foreach ($range as $start => $subrange) {
3758 $endCode = $start + (count($subrange) - 1);
3759 $cmap[] = $endCode; // endCode(s)
3761 $cmap[] = 0xFFFF; // endCode of last Segment
3762 $cmap[] = 0; // reservedPad
3763 // startCode(s)
3764 foreach ($range as $start => $subrange) {
3765 $cmap[] = $start; // startCode(s)
3767 $cmap[] = 0xFFFF; // startCode of last Segment
3768 // idDelta(s)
3769 foreach ($range as $start => $subrange) {
3770 $idDelta = -($start - $subrange[0]);
3771 $n += count($subrange);
3772 $cmap[] = $idDelta; // idDelta(s)
3774 $cmap[] = 1; // idDelta of last Segment
3775 // idRangeOffset(s)
3776 foreach ($range as $subrange) {
3777 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0
3779 $cmap[] = 0; // idRangeOffset of last Segment
3780 foreach ($range as $subrange) {
3781 foreach ($subrange as $glidx) {
3782 $cmap[] = $glidx;
3785 $cmap[] = 0; // Mapping for last character
3786 $cmapstr = '';
3787 foreach ($cmap as $cm) {
3788 $cmapstr .= pack("n", $cm);
3790 $this->add('cmap', $cmapstr);
3792 ///////////////////////////////////
3793 // glyf - Glyph data
3794 ///////////////////////////////////
3795 list($glyfOffset, $glyfLength) = $this->get_table_pos('glyf');
3796 if ($glyfLength < $this->maxStrLenRead) {
3797 $glyphData = $this->get_table('glyf');
3800 $offsets = [];
3801 $glyf = '';
3802 $pos = 0;
3803 $hmtxstr = '';
3804 $xMinT = 0;
3805 $yMinT = 0;
3806 $xMaxT = 0;
3807 $yMaxT = 0;
3808 $advanceWidthMax = 0;
3809 $minLeftSideBearing = 0;
3810 $minRightSideBearing = 0;
3811 $xMaxExtent = 0;
3812 $maxPoints = 0; // points in non-compound glyph
3813 $maxContours = 0; // contours in non-compound glyph
3814 $maxComponentPoints = 0; // points in compound glyph
3815 $maxComponentContours = 0; // contours in compound glyph
3816 $maxComponentElements = 0; // number of glyphs referenced at top level
3817 $maxComponentDepth = 0; // levels of recursion, set to 0 if font has only simple glyphs
3818 $this->glyphdata = [];
3820 foreach ($subsetglyphs as $originalGlyphIdx => $uni) {
3821 // hmtx - Horizontal Metrics
3822 $hm = $this->getHMetric($orignHmetrics, $originalGlyphIdx);
3823 $hmtxstr .= $hm;
3825 $offsets[] = $pos;
3826 $glyphPos = $this->glyphPos[$originalGlyphIdx];
3827 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;
3828 if ($glyfLength < $this->maxStrLenRead) {
3829 $data = substr($glyphData, $glyphPos, $glyphLen);
3830 } else {
3831 if ($glyphLen > 0) {
3832 $data = $this->get_chunk($glyfOffset + $glyphPos, $glyphLen);
3833 } else {
3834 $data = '';
3838 if ($glyphLen > 0) {
3839 if (_RECALC_PROFILE) {
3840 $xMin = $this->unpack_short(substr($data, 2, 2));
3841 $yMin = $this->unpack_short(substr($data, 4, 2));
3842 $xMax = $this->unpack_short(substr($data, 6, 2));
3843 $yMax = $this->unpack_short(substr($data, 8, 2));
3844 $xMinT = min($xMinT, $xMin);
3845 $yMinT = min($yMinT, $yMin);
3846 $xMaxT = max($xMaxT, $xMax);
3847 $yMaxT = max($yMaxT, $yMax);
3848 $aw = $this->unpack_short(substr($hm, 0, 2));
3849 $lsb = $this->unpack_short(substr($hm, 2, 2));
3850 $advanceWidthMax = max($advanceWidthMax, $aw);
3851 $minLeftSideBearing = min($minLeftSideBearing, $lsb);
3852 $minRightSideBearing = min($minRightSideBearing, ($aw - $lsb - ($xMax - $xMin)));
3853 $xMaxExtent = max($xMaxExtent, ($lsb + ($xMax - $xMin)));
3855 $up = unpack("n", substr($data, 0, 2));
3857 if ($glyphLen > 2 && ($up[1] & (1 << 15))) { // If number of contours <= -1 i.e. composiste glyph
3858 $pos_in_glyph = 10;
3859 $flags = GlyphOperator::MORE;
3860 $nComponentElements = 0;
3861 while ($flags & GlyphOperator::MORE) {
3862 $nComponentElements += 1; // number of glyphs referenced at top level
3863 $up = unpack("n", substr($data, $pos_in_glyph, 2));
3864 $flags = $up[1];
3865 $up = unpack("n", substr($data, $pos_in_glyph + 2, 2));
3866 $glyphIdx = $up[1];
3867 $this->glyphdata[$originalGlyphIdx]['compGlyphs'][] = $glyphIdx;
3868 $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]);
3869 $pos_in_glyph += 4;
3870 if ($flags & GlyphOperator::WORDS) {
3871 $pos_in_glyph += 4;
3872 } else {
3873 $pos_in_glyph += 2;
3875 if ($flags & GlyphOperator::SCALE) {
3876 $pos_in_glyph += 2;
3877 } else if ($flags & GlyphOperator::XYSCALE) {
3878 $pos_in_glyph += 4;
3879 } else if ($flags & GlyphOperator::TWOBYTWO) {
3880 $pos_in_glyph += 8;
3883 $maxComponentElements = max($maxComponentElements, $nComponentElements);
3884 } // Simple Glyph
3885 else if (_RECALC_PROFILE && $glyphLen > 2 && $up[1] < (1 << 15) && $up[1] > 0) { // Number of contours > 0 simple glyph
3886 $nContours = $up[1];
3887 $this->glyphdata[$originalGlyphIdx]['nContours'] = $nContours;
3888 $maxContours = max($maxContours, $nContours);
3890 // Count number of points in simple glyph
3891 $pos_in_glyph = 10 + ($nContours * 2) - 2; // Last endContourPoint
3892 $up = unpack("n", substr($data, $pos_in_glyph, 2));
3893 $points = $up[1] + 1;
3894 $this->glyphdata[$originalGlyphIdx]['nPoints'] = $points;
3895 $maxPoints = max($maxPoints, $points);
3898 $glyf .= $data;
3899 $pos += $glyphLen;
3900 if ($pos % 4 != 0) {
3901 $padding = 4 - ($pos % 4);
3902 $glyf .= str_repeat("\0", $padding);
3903 $pos += $padding;
3907 if (_RECALC_PROFILE) {
3908 foreach ($this->glyphdata as $originalGlyphIdx => $val) {
3909 $maxdepth = $depth = -1;
3910 $points = 0;
3911 $contours = 0;
3912 $this->getGlyphData($originalGlyphIdx, $maxdepth, $depth, $points, $contours);
3913 $maxComponentDepth = max($maxComponentDepth, $maxdepth);
3914 $maxComponentPoints = max($maxComponentPoints, $points);
3915 $maxComponentContours = max($maxComponentContours, $contours);
3919 $offsets[] = $pos;
3920 $this->add('glyf', $glyf);
3922 ///////////////////////////////////
3923 // hmtx - Horizontal Metrics
3924 ///////////////////////////////////
3925 $this->add('hmtx', $hmtxstr);
3927 ///////////////////////////////////
3928 // loca - Index to location
3929 ///////////////////////////////////
3930 $locastr = '';
3931 if ((($pos + 1) >> 1) > 0xFFFF) {
3932 $indexToLocFormat = 1; // long format
3933 foreach ($offsets as $offset) {
3934 $locastr .= pack("N", $offset);
3936 } else {
3937 $indexToLocFormat = 0; // short format
3938 foreach ($offsets as $offset) {
3939 $locastr .= pack("n", ($offset / 2));
3942 $this->add('loca', $locastr);
3944 ///////////////////////////////////
3945 // head - Font header
3946 ///////////////////////////////////
3947 $head = $this->get_table('head');
3948 $head = $this->_set_ushort($head, 50, $indexToLocFormat);
3949 if (_RECALC_PROFILE) {
3950 $head = $this->_set_short($head, 36, $xMinT); // for all glyph bounding boxes
3951 $head = $this->_set_short($head, 38, $yMinT); // for all glyph bounding boxes
3952 $head = $this->_set_short($head, 40, $xMaxT); // for all glyph bounding boxes
3953 $head = $this->_set_short($head, 42, $yMaxT); // for all glyph bounding boxes
3954 $head[17] = chr($head[17] & ~(1 << 4)); // Unset Bit 4 (as hdmx/LTSH tables not included)
3956 $this->add('head', $head);
3958 ///////////////////////////////////
3959 // hhea - Horizontal Header
3960 ///////////////////////////////////
3961 $hhea = $this->get_table('hhea');
3962 $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics);
3963 if (_RECALC_PROFILE) {
3964 $hhea = $this->_set_ushort($hhea, 10, $advanceWidthMax);
3965 $hhea = $this->_set_short($hhea, 12, $minLeftSideBearing);
3966 $hhea = $this->_set_short($hhea, 14, $minRightSideBearing);
3967 $hhea = $this->_set_short($hhea, 16, $xMaxExtent);
3969 $this->add('hhea', $hhea);
3971 ///////////////////////////////////
3972 // maxp - Maximum Profile
3973 ///////////////////////////////////
3974 $maxp = $this->get_table('maxp');
3975 $maxp = $this->_set_ushort($maxp, 4, $numGlyphs);
3976 if (_RECALC_PROFILE) {
3977 $maxp = $this->_set_ushort($maxp, 6, $maxPoints); // points in non-compound glyph
3978 $maxp = $this->_set_ushort($maxp, 8, $maxContours); // contours in non-compound glyph
3979 $maxp = $this->_set_ushort($maxp, 10, $maxComponentPoints); // points in compound glyph
3980 $maxp = $this->_set_ushort($maxp, 12, $maxComponentContours); // contours in compound glyph
3981 $maxp = $this->_set_ushort($maxp, 28, $maxComponentElements); // number of glyphs referenced at top level
3982 $maxp = $this->_set_ushort($maxp, 30, $maxComponentDepth); // levels of recursion, set to 0 if font has only simple glyphs
3984 $this->add('maxp', $maxp);
3986 ///////////////////////////////////
3987 // OS/2 - OS/2
3988 ///////////////////////////////////
3989 if (isset($this->tables['OS/2'])) {
3990 $os2_offset = $this->seek_table("OS/2");
3991 if (_RECALC_PROFILE) {
3992 $fsSelection = $this->get_ushort($os2_offset + 62);
3993 $fsSelection = ($fsSelection & ~(1 << 6)); // 2-byte bit field containing information concerning the nature of the font patterns
3994 // bit#0 = Italic; bit#5=Bold
3995 // Match name table's font subfamily string
3996 // Clear bit#6 used for 'Regular' and optional
3999 // NB Currently this method never subsets characters above BMP
4000 // Could set nonBMP bit according to $this->maxUni
4001 $nonBMP = $this->get_ushort($os2_offset + 46);
4002 $nonBMP = ($nonBMP & ~(1 << 9)); // Unset Bit 57 (indicates non-BMP) - for interactive forms
4004 $os2 = $this->get_table('OS/2');
4005 if (_RECALC_PROFILE) {
4006 $os2 = $this->_set_ushort($os2, 62, $fsSelection);
4007 $os2 = $this->_set_ushort($os2, 66, $fsLastCharIndex);
4008 $os2 = $this->_set_ushort($os2, 42, 0x0000); // ulCharRange (ulUnicodeRange) bits 24-31 | 16-23
4009 $os2 = $this->_set_ushort($os2, 44, 0x0000); // ulCharRange (Unicode ranges) bits 8-15 | 0-7
4010 $os2 = $this->_set_ushort($os2, 46, $nonBMP); // ulCharRange (Unicode ranges) bits 56-63 | 48-55
4011 $os2 = $this->_set_ushort($os2, 48, 0x0000); // ulCharRange (Unicode ranges) bits 40-47 | 32-39
4012 $os2 = $this->_set_ushort($os2, 50, 0x0000); // ulCharRange (Unicode ranges) bits 88-95 | 80-87
4013 $os2 = $this->_set_ushort($os2, 52, 0x0000); // ulCharRange (Unicode ranges) bits 72-79 | 64-71
4014 $os2 = $this->_set_ushort($os2, 54, 0x0000); // ulCharRange (Unicode ranges) bits 120-127 | 112-119
4015 $os2 = $this->_set_ushort($os2, 56, 0x0000); // ulCharRange (Unicode ranges) bits 104-111 | 96-103
4017 $os2 = $this->_set_ushort($os2, 46, $nonBMP); // Unset Bit 57 (indicates non-BMP) - for interactive forms
4019 $this->add('OS/2', $os2);
4022 fclose($this->fh);
4023 // Put the TTF file together
4024 $stm = '';
4025 $this->endTTFile($stm);
4027 //file_put_contents('testfont.ttf', $stm); exit;
4028 return $stm;
4031 //================================================================================
4032 // Also does SMP
4033 function makeSubsetSIP($file, &$subset, $TTCfontID = 0, $debug = false, $useOTL = 0)
4035 // mPDF 5.7.1
4036 $this->fh = fopen($file, 'rb');
4038 if (!$this->fh) {
4039 throw new \Mpdf\MpdfException('Can\'t open file ' . $file);
4042 $this->filename = $file;
4043 $this->_pos = 0;
4044 $this->useOTL = $useOTL; // mPDF 5.7.1
4045 $this->charWidths = '';
4046 $this->glyphPos = [];
4047 $this->charToGlyph = [];
4048 $this->tables = [];
4049 $this->otables = [];
4050 $this->ascent = 0;
4051 $this->descent = 0;
4052 $this->strikeoutSize = 0;
4053 $this->strikeoutPosition = 0;
4054 $this->numTTCFonts = 0;
4055 $this->TTCFonts = [];
4056 $this->skip(4);
4057 if ($TTCfontID > 0) {
4058 $this->version = $version = $this->read_ulong(); // TTC Header version now
4059 if (!in_array($version, [0x00010000, 0x00020000])) {
4060 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
4062 $this->numTTCFonts = $this->read_ulong();
4063 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
4064 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
4066 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
4067 $this->version = $version = $this->read_ulong(); // TTFont version again now
4069 $this->readTableDirectory($debug);
4071 ///////////////////////////////////
4072 // head - Font header table
4073 ///////////////////////////////////
4074 $this->seek_table("head");
4075 $this->skip(50);
4076 $indexToLocFormat = $this->read_ushort();
4077 $glyphDataFormat = $this->read_ushort();
4079 ///////////////////////////////////
4080 // hhea - Horizontal header table
4081 ///////////////////////////////////
4082 $this->seek_table("hhea");
4083 $this->skip(32);
4084 $metricDataFormat = $this->read_ushort();
4085 $orignHmetrics = $numberOfHMetrics = $this->read_ushort();
4087 ///////////////////////////////////
4088 // maxp - Maximum profile table
4089 ///////////////////////////////////
4090 $this->seek_table("maxp");
4091 $this->skip(4);
4092 $numGlyphs = $this->read_ushort();
4094 ///////////////////////////////////
4095 // cmap - Character to glyph index mapping table
4096 ///////////////////////////////////
4098 $cmap_offset = $this->seek_table("cmap");
4099 $this->skip(2);
4100 $cmapTableCount = $this->read_ushort();
4101 $unicode_cmap_offset = 0;
4102 for ($i = 0; $i < $cmapTableCount; $i++) {
4103 $platformID = $this->read_ushort();
4104 $encodingID = $this->read_ushort();
4105 $offset = $this->read_ulong();
4106 $save_pos = $this->_pos;
4107 if (($platformID == 3 && $encodingID == 10) || $platformID == 0) { // Microsoft, Unicode Format 12 table HKCS
4108 $format = $this->get_ushort($cmap_offset + $offset);
4109 if ($format == 12) {
4110 $unicode_cmap_offset = $cmap_offset + $offset;
4111 break;
4114 // mPDF 5.7.1
4115 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode
4116 $format = $this->get_ushort($cmap_offset + $offset);
4117 if ($format == 4) {
4118 $unicode_cmap_offset = $cmap_offset + $offset;
4121 $this->seek($save_pos);
4124 if (!$unicode_cmap_offset) {
4125 throw new \Mpdf\MpdfException('Font does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)');
4128 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above
4129 if ($format == 12) {
4130 $this->maxUniChar = 0;
4131 $this->seek($unicode_cmap_offset + 4);
4132 $length = $this->read_ulong();
4133 $limit = $unicode_cmap_offset + $length;
4134 $this->skip(4);
4136 $nGroups = $this->read_ulong();
4138 $glyphToChar = [];
4139 $charToGlyph = [];
4140 for ($i = 0; $i < $nGroups; $i++) {
4141 $startCharCode = $this->read_ulong();
4142 $endCharCode = $this->read_ulong();
4143 $startGlyphCode = $this->read_ulong();
4144 $offset = 0;
4145 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) {
4146 $glyph = $startGlyphCode + $offset;
4147 $offset++;
4148 // ZZZ98
4149 if ($unichar < 0x30000) {
4150 $charToGlyph[$unichar] = $glyph;
4151 $this->maxUniChar = max($unichar, $this->maxUniChar);
4152 $glyphToChar[$glyph][] = $unichar;
4156 } // mPDF 5.7.1
4157 else {
4158 $glyphToChar = [];
4159 $charToGlyph = [];
4160 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
4163 ///////////////////////////////////
4164 // mPDF 5.7.1
4165 // Map Unmapped glyphs - from $numGlyphs
4166 if ($useOTL) {
4167 $bctr = 0xE000;
4168 for ($gid = 1; $gid < $numGlyphs; $gid++) {
4169 if (!isset($glyphToChar[$gid])) {
4170 while (isset($charToGlyph[$bctr])) {
4171 $bctr++;
4172 } // Avoid overwriting a glyph already mapped in PUA
4173 // ZZZ98
4174 if ($bctr > 0xF8FF && $bctr < 0x2CEB0) {
4175 $bctr = 0x2CEB0;
4176 while (isset($charToGlyph[$bctr])) {
4177 $bctr++;
4180 $glyphToChar[$gid][] = $bctr;
4181 $charToGlyph[$bctr] = $gid;
4182 $this->maxUniChar = max($bctr, $this->maxUniChar);
4183 $bctr++;
4187 ///////////////////////////////////
4188 ///////////////////////////////////
4189 // hmtx - Horizontal metrics table
4190 ///////////////////////////////////
4191 $scale = 1; // not used here
4192 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale);
4194 ///////////////////////////////////
4195 // loca - Index to location
4196 ///////////////////////////////////
4197 $this->getLOCA($indexToLocFormat, $numGlyphs);
4199 ///////////////////////////////////////////////////////////////////
4201 $glyphMap = [0 => 0];
4202 $glyphSet = [0 => 0];
4203 $codeToGlyph = [];
4204 // Set a substitute if ASCII characters do not have glyphs
4205 if (isset($charToGlyph[0x3F])) {
4206 $subs = $charToGlyph[0x3F];
4207 } // Question mark
4208 else {
4209 $subs = $charToGlyph[32];
4211 foreach ($subset as $code) {
4212 if (isset($charToGlyph[$code])) {
4213 $originalGlyphIdx = $charToGlyph[$code];
4214 } else if ($code < 128) {
4215 $originalGlyphIdx = $subs;
4216 } else {
4217 $originalGlyphIdx = 0;
4219 if (!isset($glyphSet[$originalGlyphIdx])) {
4220 $glyphSet[$originalGlyphIdx] = count($glyphMap);
4221 $glyphMap[] = $originalGlyphIdx;
4223 $codeToGlyph[$code] = $glyphSet[$originalGlyphIdx];
4226 list($start, $dummy) = $this->get_table_pos('glyf');
4228 $n = 0;
4229 while ($n < count($glyphMap)) {
4230 $originalGlyphIdx = $glyphMap[$n];
4231 $glyphPos = $this->glyphPos[$originalGlyphIdx];
4232 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;
4233 $n += 1;
4234 if (!$glyphLen) {
4235 continue;
4237 $this->seek($start + $glyphPos);
4238 $numberOfContours = $this->read_short();
4239 if ($numberOfContours < 0) {
4240 $this->skip(8);
4241 $flags = GlyphOperator::MORE;
4242 while ($flags & GlyphOperator::MORE) {
4243 $flags = $this->read_ushort();
4244 $glyphIdx = $this->read_ushort();
4245 if (!isset($glyphSet[$glyphIdx])) {
4246 $glyphSet[$glyphIdx] = count($glyphMap);
4247 $glyphMap[] = $glyphIdx;
4249 if ($flags & GlyphOperator::WORDS) {
4250 $this->skip(4);
4251 } else {
4252 $this->skip(2);
4254 if ($flags & GlyphOperator::SCALE) {
4255 $this->skip(2);
4256 } else if ($flags & GlyphOperator::XYSCALE) {
4257 $this->skip(4);
4258 } else if ($flags & GlyphOperator::TWOBYTWO) {
4259 $this->skip(8);
4265 $numGlyphs = $n = count($glyphMap);
4266 $numberOfHMetrics = $n;
4268 ///////////////////////////////////
4269 // name
4270 ///////////////////////////////////
4271 // MS spec says that "Platform and encoding ID's in the name table should be consistent with those in the cmap table.
4272 // If they are not, the font will not load in Windows"
4273 // Doesn't seem to be a problem?
4274 ///////////////////////////////////
4275 // Needs to have a name entry in 3,0 (e.g. symbol) - original font will be 3,1 (i.e. Unicode)
4276 $name = $this->get_table('name');
4277 $name_offset = $this->seek_table("name");
4278 $format = $this->read_ushort();
4279 $numRecords = $this->read_ushort();
4280 $string_data_offset = $name_offset + $this->read_ushort();
4281 for ($i = 0; $i < $numRecords; $i++) {
4282 $platformId = $this->read_ushort();
4283 $encodingId = $this->read_ushort();
4284 if ($platformId == 3 && $encodingId == 1) {
4285 $pos = 6 + ($i * 12) + 2;
4286 $name = $this->_set_ushort($name, $pos, 0x00); // Change encoding to 3,0 rather than 3,1
4288 $this->skip(8);
4290 $this->add('name', $name);
4292 ///////////////////////////////////
4293 // OS/2
4294 ///////////////////////////////////
4295 if (isset($this->tables['OS/2'])) {
4296 $os2 = $this->get_table('OS/2');
4297 $os2 = $this->_set_ushort($os2, 42, 0x00); // ulCharRange (Unicode ranges)
4298 $os2 = $this->_set_ushort($os2, 44, 0x00); // ulCharRange (Unicode ranges)
4299 $os2 = $this->_set_ushort($os2, 46, 0x00); // ulCharRange (Unicode ranges)
4300 $os2 = $this->_set_ushort($os2, 48, 0x00); // ulCharRange (Unicode ranges)
4302 $os2 = $this->_set_ushort($os2, 50, 0x00); // ulCharRange (Unicode ranges)
4303 $os2 = $this->_set_ushort($os2, 52, 0x00); // ulCharRange (Unicode ranges)
4304 $os2 = $this->_set_ushort($os2, 54, 0x00); // ulCharRange (Unicode ranges)
4305 $os2 = $this->_set_ushort($os2, 56, 0x00); // ulCharRange (Unicode ranges)
4306 // Set Symbol character only in ulCodePageRange
4307 $os2 = $this->_set_ushort($os2, 78, 0x8000); // ulCodePageRange = Bit #31 Symbol **** 78 = Bit 16-31
4308 $os2 = $this->_set_ushort($os2, 80, 0x0000); // ulCodePageRange = Bit #31 Symbol **** 80 = Bit 0-15
4309 $os2 = $this->_set_ushort($os2, 82, 0x0000); // ulCodePageRange = Bit #32- Symbol **** 82 = Bits 48-63
4310 $os2 = $this->_set_ushort($os2, 84, 0x0000); // ulCodePageRange = Bit #32- Symbol **** 84 = Bits 32-47
4312 $os2 = $this->_set_ushort($os2, 64, 0x01); // FirstCharIndex
4313 $os2 = $this->_set_ushort($os2, 66, count($subset)); // LastCharIndex
4314 // Set PANOSE first bit to 5 for Symbol
4315 $os2 = $this->splice($os2, 32, chr(5) . chr(0) . chr(1) . chr(0) . chr(1) . chr(0) . chr(0) . chr(0) . chr(0) . chr(0));
4316 $this->add('OS/2', $os2);
4319 ///////////////////////////////////
4320 //tables copied from the original
4321 ///////////////////////////////////
4322 $tags = ['cvt ', 'fpgm', 'prep', 'gasp'];
4323 foreach ($tags as $tag) { // 1.02
4324 if (isset($this->tables[$tag])) {
4325 $this->add($tag, $this->get_table($tag));
4329 ///////////////////////////////////
4330 // post - PostScript
4331 ///////////////////////////////////
4332 if (isset($this->tables['post'])) {
4333 $opost = $this->get_table('post');
4334 $post = "\x00\x03\x00\x00" . substr($opost, 4, 12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
4336 $this->add('post', $post);
4338 ///////////////////////////////////
4339 // hhea - Horizontal Header
4340 ///////////////////////////////////
4341 $hhea = $this->get_table('hhea');
4342 $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics);
4343 $this->add('hhea', $hhea);
4345 ///////////////////////////////////
4346 // maxp - Maximum Profile
4347 ///////////////////////////////////
4348 $maxp = $this->get_table('maxp');
4349 $maxp = $this->_set_ushort($maxp, 4, $numGlyphs);
4350 $this->add('maxp', $maxp);
4352 ///////////////////////////////////
4353 // CMap table Formats [1,0,]6 and [3,0,]4
4354 ///////////////////////////////////
4355 ///////////////////////////////////
4356 // Sort CID2GID map into segments of contiguous codes
4357 ///////////////////////////////////
4358 $rangeid = 0;
4359 $range = [];
4360 $prevcid = -2;
4361 $prevglidx = -1;
4362 // for each character
4363 foreach ($subset as $cid => $code) {
4364 $glidx = $codeToGlyph[$code];
4365 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) {
4366 $range[$rangeid][] = $glidx;
4367 } else {
4368 // new range
4369 $rangeid = $cid;
4370 $range[$rangeid] = [];
4371 $range[$rangeid][] = $glidx;
4373 $prevcid = $cid;
4374 $prevglidx = $glidx;
4376 // cmap - Character to glyph mapping
4377 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF
4378 $searchRange = 1;
4379 $entrySelector = 0;
4380 while ($searchRange * 2 <= $segCount) {
4381 $searchRange = $searchRange * 2;
4382 $entrySelector = $entrySelector + 1;
4384 $searchRange = $searchRange * 2;
4385 $rangeShift = $segCount * 2 - $searchRange;
4386 $length = 16 + (8 * $segCount) + ($numGlyphs + 1);
4387 $cmap = [
4388 4, $length, 0, // Format 4 Mapping subtable: format, length, language
4389 $segCount * 2,
4390 $searchRange,
4391 $entrySelector,
4392 $rangeShift];
4394 // endCode(s)
4395 foreach ($range as $start => $subrange) {
4396 $endCode = $start + (count($subrange) - 1);
4397 $cmap[] = $endCode; // endCode(s)
4399 $cmap[] = 0xFFFF; // endCode of last Segment
4400 $cmap[] = 0; // reservedPad
4401 // startCode(s)
4402 foreach ($range as $start => $subrange) {
4403 $cmap[] = $start; // startCode(s)
4405 $cmap[] = 0xFFFF; // startCode of last Segment
4406 // idDelta(s)
4407 foreach ($range as $start => $subrange) {
4408 $idDelta = -($start - $subrange[0]);
4409 $n += count($subrange);
4410 $cmap[] = $idDelta; // idDelta(s)
4412 $cmap[] = 1; // idDelta of last Segment
4413 // idRangeOffset(s)
4414 foreach ($range as $subrange) {
4415 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0
4417 $cmap[] = 0; // idRangeOffset of last Segment
4418 foreach ($range as $subrange) {
4419 foreach ($subrange as $glidx) {
4420 $cmap[] = $glidx;
4423 $cmap[] = 0; // Mapping for last character
4424 $cmapstr4 = '';
4425 foreach ($cmap as $cm) {
4426 $cmapstr4 .= pack("n", $cm);
4429 ///////////////////////////////////
4430 // cmap - Character to glyph mapping
4431 ///////////////////////////////////
4432 $entryCount = count($subset);
4433 $length = 10 + $entryCount * 2;
4435 $off = 20 + $length;
4436 $hoff = $off >> 16;
4437 $loff = $off & 0xFFFF;
4439 $cmap = [0, 2, // Index : version, number of subtables
4440 1, 0, // Subtable : platform, encoding
4441 0, 20, // offset (hi,lo)
4442 3, 0, // Subtable : platform, encoding // See note above for 'name'
4443 $hoff, $loff, // offset (hi,lo)
4444 6, $length, // Format 6 Mapping table: format, length
4445 0, 1, // language, First char code
4446 $entryCount,
4448 $cmapstr = '';
4449 foreach ($subset as $code) {
4450 $cmap[] = $codeToGlyph[$code];
4452 foreach ($cmap as $cm) {
4453 $cmapstr .= pack("n", $cm);
4455 $cmapstr .= $cmapstr4;
4456 $this->add('cmap', $cmapstr);
4458 ///////////////////////////////////
4459 // hmtx - Horizontal Metrics
4460 ///////////////////////////////////
4461 $hmtxstr = '';
4462 for ($n = 0; $n < $numGlyphs; $n++) {
4463 $originalGlyphIdx = $glyphMap[$n];
4464 $hm = $this->getHMetric($orignHmetrics, $originalGlyphIdx);
4465 $hmtxstr .= $hm;
4467 $this->add('hmtx', $hmtxstr);
4469 ///////////////////////////////////
4470 // glyf - Glyph data
4471 ///////////////////////////////////
4472 list($glyfOffset, $glyfLength) = $this->get_table_pos('glyf');
4473 if ($glyfLength < $this->maxStrLenRead) {
4474 $glyphData = $this->get_table('glyf');
4477 $offsets = [];
4478 $glyf = '';
4479 $pos = 0;
4480 for ($n = 0; $n < $numGlyphs; $n++) {
4481 $offsets[] = $pos;
4482 $originalGlyphIdx = $glyphMap[$n];
4483 $glyphPos = $this->glyphPos[$originalGlyphIdx];
4484 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;
4485 if ($glyfLength < $this->maxStrLenRead) {
4486 $data = substr($glyphData, $glyphPos, $glyphLen);
4487 } else {
4488 if ($glyphLen > 0) {
4489 $data = $this->get_chunk($glyfOffset + $glyphPos, $glyphLen);
4490 } else {
4491 $data = '';
4494 if ($glyphLen > 0) {
4495 $up = unpack("n", substr($data, 0, 2));
4497 if ($glyphLen > 2 && ($up[1] & (1 << 15))) {
4498 $pos_in_glyph = 10;
4499 $flags = GlyphOperator::MORE;
4500 while ($flags & GlyphOperator::MORE) {
4501 $up = unpack("n", substr($data, $pos_in_glyph, 2));
4502 $flags = $up[1];
4503 $up = unpack("n", substr($data, $pos_in_glyph + 2, 2));
4504 $glyphIdx = $up[1];
4505 $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]);
4506 $pos_in_glyph += 4;
4507 if ($flags & GlyphOperator::WORDS) {
4508 $pos_in_glyph += 4;
4509 } else {
4510 $pos_in_glyph += 2;
4512 if ($flags & GlyphOperator::SCALE) {
4513 $pos_in_glyph += 2;
4514 } else if ($flags & GlyphOperator::XYSCALE) {
4515 $pos_in_glyph += 4;
4516 } else if ($flags & GlyphOperator::TWOBYTWO) {
4517 $pos_in_glyph += 8;
4521 $glyf .= $data;
4522 $pos += $glyphLen;
4523 if ($pos % 4 != 0) {
4524 $padding = 4 - ($pos % 4);
4525 $glyf .= str_repeat("\0", $padding);
4526 $pos += $padding;
4529 $offsets[] = $pos;
4530 $this->add('glyf', $glyf);
4532 ///////////////////////////////////
4533 // loca - Index to location
4534 ///////////////////////////////////
4535 $locastr = '';
4536 if ((($pos + 1) >> 1) > 0xFFFF) {
4537 $indexToLocFormat = 1; // long format
4538 foreach ($offsets as $offset) {
4539 $locastr .= pack("N", $offset);
4541 } else {
4542 $indexToLocFormat = 0; // short format
4543 foreach ($offsets as $offset) {
4544 $locastr .= pack("n", ($offset / 2));
4547 $this->add('loca', $locastr);
4549 ///////////////////////////////////
4550 // head - Font header
4551 ///////////////////////////////////
4552 $head = $this->get_table('head');
4553 $head = $this->_set_ushort($head, 50, $indexToLocFormat);
4554 $this->add('head', $head);
4556 fclose($this->fh);
4558 // Put the TTF file together
4559 $stm = '';
4560 $this->endTTFile($stm);
4562 //file_put_contents('testfont.ttf', $stm); exit;
4563 return $stm;
4566 //////////////////////////////////////////////////////////////////////////////////
4567 // Recursively get composite glyph data
4568 function getGlyphData($originalGlyphIdx, &$maxdepth, &$depth, &$points, &$contours)
4570 $depth++;
4571 $maxdepth = max($maxdepth, $depth);
4572 if (count($this->glyphdata[$originalGlyphIdx]['compGlyphs'])) {
4573 foreach ($this->glyphdata[$originalGlyphIdx]['compGlyphs'] as $glyphIdx) {
4574 $this->getGlyphData($glyphIdx, $maxdepth, $depth, $points, $contours);
4576 } else if (($this->glyphdata[$originalGlyphIdx]['nContours'] > 0) && $depth > 0) { // simple
4577 $contours += $this->glyphdata[$originalGlyphIdx]['nContours'];
4578 $points += $this->glyphdata[$originalGlyphIdx]['nPoints'];
4580 $depth--;
4583 //////////////////////////////////////////////////////////////////////////////////
4584 // Recursively get composite glyphs
4585 function getGlyphs($originalGlyphIdx, &$start, &$glyphSet, &$subsetglyphs)
4587 $glyphPos = $this->glyphPos[$originalGlyphIdx];
4588 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;
4589 if (!$glyphLen) {
4590 return;
4592 $this->seek($start + $glyphPos);
4593 $numberOfContours = $this->read_short();
4594 if ($numberOfContours < 0) {
4595 $this->skip(8);
4596 $flags = GlyphOperator::MORE;
4597 while ($flags & GlyphOperator::MORE) {
4598 $flags = $this->read_ushort();
4599 $glyphIdx = $this->read_ushort();
4600 if (!isset($glyphSet[$glyphIdx])) {
4601 $glyphSet[$glyphIdx] = count($subsetglyphs); // old glyphID to new glyphID
4602 $subsetglyphs[$glyphIdx] = true;
4604 $savepos = ftell($this->fh);
4605 $this->getGlyphs($glyphIdx, $start, $glyphSet, $subsetglyphs);
4606 $this->seek($savepos);
4607 if ($flags & GlyphOperator::WORDS) {
4608 $this->skip(4);
4609 } else {
4610 $this->skip(2);
4612 if ($flags & GlyphOperator::SCALE) {
4613 $this->skip(2);
4614 } else if ($flags & GlyphOperator::XYSCALE) {
4615 $this->skip(4);
4616 } else if ($flags & GlyphOperator::TWOBYTWO) {
4617 $this->skip(8);
4623 //////////////////////////////////////////////////////////////////////////////////
4625 function getHMTX($numberOfHMetrics, $numGlyphs, &$glyphToChar, $scale)
4627 $start = $this->seek_table("hmtx");
4628 $aw = 0;
4629 $this->charWidths = str_pad('', 256 * 256 * 2, "\x00");
4630 if ($this->maxUniChar > 65536) {
4631 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00");
4632 } // Plane 1 SMP
4633 if ($this->maxUniChar > 131072) {
4634 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00");
4635 } // Plane 2 SMP
4636 $nCharWidths = 0;
4637 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) {
4638 $data = $this->get_chunk($start, ($numberOfHMetrics * 4));
4639 $arr = unpack("n*", $data);
4640 } else {
4641 $this->seek($start);
4643 for ($glyph = 0; $glyph < $numberOfHMetrics; $glyph++) {
4644 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) {
4645 $aw = $arr[($glyph * 2) + 1];
4646 } else {
4647 $aw = $this->read_ushort();
4648 $lsb = $this->read_ushort();
4650 if (isset($glyphToChar[$glyph]) || $glyph == 0) {
4651 if ($aw >= (1 << 15)) {
4652 $aw = 0;
4653 } // 1.03 Some (arabic) fonts have -ve values for width
4654 // although should be unsigned value - comes out as e.g. 65108 (intended -50)
4655 if ($glyph == 0) {
4656 $this->defaultWidth = $scale * $aw;
4657 continue;
4659 foreach ($glyphToChar[$glyph] as $char) {
4660 if ($char != 0 && $char != 65535) {
4661 $w = intval(round($scale * $aw));
4662 if ($w == 0) {
4663 $w = 65535;
4665 if ($char < 196608) {
4666 $this->charWidths[$char * 2] = chr($w >> 8);
4667 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF);
4668 $nCharWidths++;
4675 $data = $this->get_chunk(($start + $numberOfHMetrics * 4), ($numGlyphs * 2));
4676 $arr = unpack("n*", $data);
4677 $diff = $numGlyphs - $numberOfHMetrics;
4678 $w = intval(round($scale * $aw));
4679 if ($w == 0) {
4680 $w = 65535;
4682 for ($pos = 0; $pos < $diff; $pos++) {
4683 $glyph = $pos + $numberOfHMetrics;
4684 if (isset($glyphToChar[$glyph])) {
4685 foreach ($glyphToChar[$glyph] as $char) {
4686 if ($char != 0 && $char != 65535) {
4687 if ($char < 196608) {
4688 $this->charWidths[$char * 2] = chr($w >> 8);
4689 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF);
4690 $nCharWidths++;
4696 // NB 65535 is a set width of 0
4697 // First bytes define number of chars in font
4698 $this->charWidths[0] = chr($nCharWidths >> 8);
4699 $this->charWidths[1] = chr($nCharWidths & 0xFF);
4702 function getHMetric($numberOfHMetrics, $gid)
4704 $start = $this->seek_table("hmtx");
4705 if ($gid < $numberOfHMetrics) {
4706 $this->seek($start + ($gid * 4));
4707 $hm = fread($this->fh, 4);
4708 } else {
4709 $this->seek($start + (($numberOfHMetrics - 1) * 4));
4710 $hm = fread($this->fh, 2);
4711 $this->seek($start + ($numberOfHMetrics * 2) + ($gid * 2));
4712 $hm .= fread($this->fh, 2);
4715 return $hm;
4718 function getLOCA($indexToLocFormat, $numGlyphs)
4720 $start = $this->seek_table('loca');
4721 $this->glyphPos = [];
4722 if ($indexToLocFormat == 0) {
4723 $data = $this->get_chunk($start, ($numGlyphs * 2) + 2);
4724 $arr = unpack("n*", $data);
4725 for ($n = 0; $n <= $numGlyphs; $n++) {
4726 $this->glyphPos[] = ($arr[$n + 1] * 2);
4728 } else if ($indexToLocFormat == 1) {
4729 $data = $this->get_chunk($start, ($numGlyphs * 4) + 4);
4730 $arr = unpack("N*", $data);
4731 for ($n = 0; $n <= $numGlyphs; $n++) {
4732 $this->glyphPos[] = ($arr[$n + 1]);
4734 } else {
4735 throw new \Mpdf\MpdfException('Unknown location table format ' . $indexToLocFormat);
4739 // CMAP Format 4
4740 function getCMAP4($unicode_cmap_offset, &$glyphToChar, &$charToGlyph)
4742 $this->maxUniChar = 0;
4743 $this->seek($unicode_cmap_offset + 2);
4744 $length = $this->read_ushort();
4745 $limit = $unicode_cmap_offset + $length;
4746 $this->skip(2);
4748 $segCount = $this->read_ushort() / 2;
4749 $this->skip(6);
4750 $endCount = [];
4751 for ($i = 0; $i < $segCount; $i++) {
4752 $endCount[] = $this->read_ushort();
4754 $this->skip(2);
4755 $startCount = [];
4756 for ($i = 0; $i < $segCount; $i++) {
4757 $startCount[] = $this->read_ushort();
4759 $idDelta = [];
4760 for ($i = 0; $i < $segCount; $i++) {
4761 $idDelta[] = $this->read_short();
4762 } // ???? was unsigned short
4763 $idRangeOffset_start = $this->_pos;
4764 $idRangeOffset = [];
4765 for ($i = 0; $i < $segCount; $i++) {
4766 $idRangeOffset[] = $this->read_ushort();
4769 for ($n = 0; $n < $segCount; $n++) {
4770 $endpoint = ($endCount[$n] + 1);
4771 for ($unichar = $startCount[$n]; $unichar < $endpoint; $unichar++) {
4772 if ($idRangeOffset[$n] == 0) {
4773 $glyph = ($unichar + $idDelta[$n]) & 0xFFFF;
4774 } else {
4775 $offset = ($unichar - $startCount[$n]) * 2 + $idRangeOffset[$n];
4776 $offset = $idRangeOffset_start + 2 * $n + $offset;
4777 if ($offset >= $limit) {
4778 $glyph = 0;
4779 } else {
4780 $glyph = $this->get_ushort($offset);
4781 if ($glyph != 0) {
4782 $glyph = ($glyph + $idDelta[$n]) & 0xFFFF;
4786 $charToGlyph[$unichar] = $glyph;
4787 if ($unichar < 196608) {
4788 $this->maxUniChar = max($unichar, $this->maxUniChar);
4790 $glyphToChar[$glyph][] = $unichar;
4795 // Put the TTF file together
4796 function endTTFile(&$stm)
4798 $stm = '';
4799 $numTables = count($this->otables);
4800 $searchRange = 1;
4801 $entrySelector = 0;
4802 while ($searchRange * 2 <= $numTables) {
4803 $searchRange = $searchRange * 2;
4804 $entrySelector = $entrySelector + 1;
4806 $searchRange = $searchRange * 16;
4807 $rangeShift = $numTables * 16 - $searchRange;
4809 // Header
4810 if (_TTF_MAC_HEADER) {
4811 $stm .= (pack("Nnnnn", 0x74727565, $numTables, $searchRange, $entrySelector, $rangeShift)); // Mac
4812 } else {
4813 $stm .= (pack("Nnnnn", 0x00010000, $numTables, $searchRange, $entrySelector, $rangeShift)); // Windows
4816 // Table directory
4817 $tables = $this->otables;
4818 ksort($tables);
4819 $offset = 12 + $numTables * 16;
4820 foreach ($tables as $tag => $data) {
4821 if ($tag == 'head') {
4822 $head_start = $offset;
4824 $stm .= $tag;
4825 $checksum = $this->calcChecksum($data);
4826 $stm .= pack("nn", $checksum[0], $checksum[1]);
4827 $stm .= pack("NN", $offset, strlen($data));
4828 $paddedLength = (strlen($data) + 3) & ~3;
4829 $offset = $offset + $paddedLength;
4832 // Table data
4833 foreach ($tables as $tag => $data) {
4834 $data .= "\0\0\0";
4835 $stm .= substr($data, 0, (strlen($data) & ~3));
4838 $checksum = $this->calcChecksum($stm);
4839 $checksum = $this->sub32([0xB1B0, 0xAFBA], $checksum);
4840 $chk = pack("nn", $checksum[0], $checksum[1]);
4841 $stm = $this->splice($stm, ($head_start + 8), $chk);
4843 return $stm;
4846 function repackageTTF($file, $TTCfontID = 0, $debug = false, $useOTL = false)
4848 // mPDF 5.7.1
4849 // (Does not called for subsets)
4850 $this->useOTL = $useOTL; // mPDF 5.7.1
4851 $this->filename = $file;
4852 $this->fh = fopen($file, 'rb');
4854 if (!$this->fh) {
4855 throw new \Mpdf\MpdfException('Can\'t open file ' . $file);
4858 $this->_pos = 0;
4859 $this->charWidths = '';
4860 $this->glyphPos = [];
4861 $this->charToGlyph = [];
4862 $this->tables = [];
4863 $this->otables = [];
4864 $this->ascent = 0;
4865 $this->descent = 0;
4866 $this->strikeoutSize = 0;
4867 $this->strikeoutPosition = 0;
4868 $this->numTTCFonts = 0;
4869 $this->TTCFonts = [];
4870 $this->skip(4);
4871 $this->maxUni = 0;
4872 if ($TTCfontID > 0) {
4873 $this->version = $version = $this->read_ulong(); // TTC Header version now
4874 if (!in_array($version, [0x00010000, 0x00020000])) {
4875 throw new \Mpdf\MpdfException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file);
4877 $this->numTTCFonts = $this->read_ulong();
4878 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
4879 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
4881 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
4882 $this->version = $version = $this->read_ulong(); // TTFont version again now
4884 $this->readTableDirectory($debug);
4885 $tags = ['OS/2', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'cvt ', 'fpgm', 'gasp', 'prep'];
4887 foreach ($tags as $tag) {
4888 if (isset($this->tables[$tag])) {
4889 $this->add($tag, $this->get_table($tag));
4893 // mPDF 5.7.1
4894 if ($useOTL) {
4895 ///////////////////////////////////
4896 // maxp - Maximum profile table
4897 ///////////////////////////////////
4898 $this->seek_table("maxp");
4899 $this->skip(4);
4900 $numGlyphs = $this->read_ushort();
4902 ///////////////////////////////////
4903 // cmap - Character to glyph index mapping table
4904 ///////////////////////////////////
4905 $cmap_offset = $this->seek_table("cmap");
4906 $this->skip(2);
4907 $cmapTableCount = $this->read_ushort();
4908 $unicode_cmap_offset = 0;
4909 for ($i = 0; $i < $cmapTableCount; $i++) {
4910 $platformID = $this->read_ushort();
4911 $encodingID = $this->read_ushort();
4912 $offset = $this->read_ulong();
4913 $save_pos = $this->_pos;
4914 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode
4915 $format = $this->get_ushort($cmap_offset + $offset);
4916 if ($format == 4) {
4917 $unicode_cmap_offset = $cmap_offset + $offset;
4918 break;
4921 $this->seek($save_pos);
4924 if (!$unicode_cmap_offset) {
4925 throw new \Mpdf\MpdfException('Font (' . $this->filename . ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)');
4928 $glyphToChar = [];
4929 $charToGlyph = [];
4930 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
4932 ///////////////////////////////////
4933 // Map Unmapped glyphs - from $numGlyphs
4934 $bctr = 0xE000;
4935 for ($gid = 1; $gid < $numGlyphs; $gid++) {
4936 if (!isset($glyphToChar[$gid])) {
4937 while (isset($charToGlyph[$bctr])) {
4938 $bctr++;
4939 } // Avoid overwriting a glyph already mapped in PUA (6,400)
4940 if ($bctr > 0xF8FF) {
4941 throw new \Mpdf\MpdfException("Problem. Trying to repackage TF file; not enough space for unmapped glyphs");
4943 $glyphToChar[$gid][] = $bctr;
4944 $charToGlyph[$bctr] = $gid;
4945 $bctr++;
4948 ///////////////////////////////////
4949 ///////////////////////////////////
4950 // Sort CID2GID map into segments of contiguous codes
4951 ///////////////////////////////////
4952 unset($charToGlyph[65535]);
4953 unset($charToGlyph[0]);
4954 ksort($charToGlyph);
4955 $rangeid = 0;
4956 $range = [];
4957 $prevcid = -2;
4958 $prevglidx = -1;
4959 // for each character
4960 foreach ($charToGlyph as $cid => $glidx) {
4961 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) {
4962 $range[$rangeid][] = $glidx;
4963 } else {
4964 // new range
4965 $rangeid = $cid;
4966 $range[$rangeid] = [];
4967 $range[$rangeid][] = $glidx;
4969 $prevcid = $cid;
4970 $prevglidx = $glidx;
4973 ///////////////////////////////////
4974 // CMap table
4975 ///////////////////////////////////
4976 // cmap - Character to glyph mapping
4977 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF
4978 $searchRange = 1;
4979 $entrySelector = 0;
4980 while ($searchRange * 2 <= $segCount) {
4981 $searchRange = $searchRange * 2;
4982 $entrySelector = $entrySelector + 1;
4984 $searchRange = $searchRange * 2;
4985 $rangeShift = $segCount * 2 - $searchRange;
4986 $length = 16 + (8 * $segCount) + ($numGlyphs + 1);
4987 $cmap = [0, 3, // Index : version, number of encoding subtables
4988 0, 0, // Encoding Subtable : platform (UNI=0), encoding 0
4989 0, 28, // Encoding Subtable : offset (hi,lo)
4990 0, 3, // Encoding Subtable : platform (UNI=0), encoding 3
4991 0, 28, // Encoding Subtable : offset (hi,lo)
4992 3, 1, // Encoding Subtable : platform (MS=3), encoding 1
4993 0, 28, // Encoding Subtable : offset (hi,lo)
4994 4, $length, 0, // Format 4 Mapping subtable: format, length, language
4995 $segCount * 2,
4996 $searchRange,
4997 $entrySelector,
4998 $rangeShift];
5000 // endCode(s)
5001 foreach ($range as $start => $subrange) {
5002 $endCode = $start + (count($subrange) - 1);
5003 $cmap[] = $endCode; // endCode(s)
5005 $cmap[] = 0xFFFF; // endCode of last Segment
5006 $cmap[] = 0; // reservedPad
5007 // startCode(s)
5008 foreach ($range as $start => $subrange) {
5009 $cmap[] = $start; // startCode(s)
5011 $cmap[] = 0xFFFF; // startCode of last Segment
5012 // idDelta(s)
5013 foreach ($range as $start => $subrange) {
5014 $idDelta = -($start - $subrange[0]);
5015 //$n += count($subrange); // ?? Line not required
5016 $cmap[] = $idDelta; // idDelta(s)
5018 $cmap[] = 1; // idDelta of last Segment
5019 // idRangeOffset(s)
5020 foreach ($range as $subrange) {
5021 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0
5023 $cmap[] = 0; // idRangeOffset of last Segment
5024 foreach ($range as $subrange) {
5025 foreach ($subrange as $glidx) {
5026 $cmap[] = $glidx;
5029 $cmap[] = 0; // Mapping for last character
5030 $cmapstr = '';
5031 foreach ($cmap as $cm) {
5032 $cmapstr .= pack("n", $cm);
5034 $this->add('cmap', $cmapstr);
5035 } else {
5036 $this->add('cmap', $this->get_table('cmap'));
5039 fclose($this->fh);
5040 $stm = '';
5041 $this->endTTFile($stm);
5043 return $stm;