PluginProbe
PDF & Print by BestWebSoft – WordPress Posts and Pages PDF Generator Plugin / 1.8.2
PDF & Print by BestWebSoft – WordPress Posts and Pages PDF Generator Plugin v1.8.2
trunk 1.5 1.6 1.7 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 All 71 releases
pdf-print / mpdf / classes / otl.php

otl.php in PDF & Print by BestWebSoft – WordPress Posts and Pages PDF Generator Plugin 1.8.2, at mpdf/classes/otl.php

5,719 lines 243.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 define("_OTL_OLD_SPEC_COMPAT_1", true);
4
5 define("_DICT_NODE_TYPE_SPLIT", 0x01);
6 define("_DICT_NODE_TYPE_LINEAR", 0x02);
7 define("_DICT_INTERMEDIATE_MATCH", 0x03);
8 define("_DICT_FINAL_MATCH", 0x04);
9
10
11
12 class otl {
13
14 var $mpdf;
15 var $arabLeftJoining;
16 var $arabRightJoining;
17 var $arabTransparentJoin;
18 var $arabTransparent;
19 var $GSUBdata;
20 var $GPOSdata;
21 var $GSUBfont;
22 var $fontkey;
23 var $ttfOTLdata;
24 var $glyphIDtoUni;
25 var $_pos;
26 var $GSUB_offset;
27 var $GPOS_offset;
28 var $MarkAttachmentType;
29 var $MarkGlyphSets;
30 var $GlyphClassMarks;
31 var $GlyphClassLigatures;
32 var $GlyphClassBases;
33 var $GlyphClassComponents;
34 var $Ignores;
35 var $LuCoverage;
36 var $OTLdata;
37 var $assocLigs;
38 var $assocMarks;
39 var $shaper;
40 var $restrictToSyllable;
41 var $lbdicts; // Line-breaking dictionaries
42 var $LuDataCache;
43
44 var $debugOTL = false;
45
46 function otl(&$mpdf) {
47 $this->mpdf = $mpdf;
48
49 $this->arabic_initialise();
50 $this->current_fh = '';
51
52 $this->lbdicts = array();
53 $this->LuDataCache = array();
54 }
55
56 ////////////////////////////////////////////////////////////////
57 ////////////////////////////////////////////////////////////////
58 ////////// APPLY OTL ////////////////////////////
59 ////////////////////////////////////////////////////////////////
60 ////////////////////////////////////////////////////////////////
61
62 function applyOTL($str, $useOTL) {
63 $this->OTLdata = array();
64 if (trim($str)=='') { return $str; }
65 if (!$useOTL) { return $str; }
66
67 // 1. Load GDEF data
68 //==============================
69 $this->fontkey = $this->mpdf->CurrentFont['fontkey'];
70 $this->glyphIDtoUni = $this->mpdf->CurrentFont['glyphIDtoUni'];
71 if (!isset($this->GDEFdata[$this->fontkey])) {
72 include(_MPDF_TTFONTDATAPATH.$this->fontkey.'.GDEFdata.php');
73 $this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'] = $GSUB_offset;
74 $this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'] = $GPOS_offset;
75 $this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'] = $GSUB_length;
76 $this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'] = $MarkAttachmentType;
77 $this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'] = $MarkGlyphSets;
78 $this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'] = $GlyphClassMarks;
79 $this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'] = $GlyphClassLigatures;
80 $this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'] = $GlyphClassComponents;
81 $this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'] = $GlyphClassBases;
82 }
83 else {
84 $this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'];
85 $this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'];
86 $this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'];
87 $this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'];
88 $this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'];
89 $this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'];
90 $this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'];
91 $this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'];
92 $this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'];
93 }
94
95 // 2. Prepare string as HEX string and Analyse character properties
96 //=================================================================
97 $earr = $this->mpdf->UTF8StringToArray($str, false);
98
99 $scriptblock = 0;
100 $scriptblocks = array();
101 $scriptblocks[0] = 0;
102 $vstr = '';
103 $OTLdata = array();
104 $subchunk = 0;
105 $charctr = 0;
106 foreach($earr as $char) {
107 $ucd_record = UCDN::get_ucd_record($char);
108 $sbl = $ucd_record[6];
109
110 // Special case - Arabic End of Ayah
111 if ($char==1757) { $sbl = UCDN::SCRIPT_ARABIC; }
112
113 if ($sbl && $sbl != 40 && $sbl != 102) {
114 if ($scriptblock == 0) { $scriptblock = $sbl; $scriptblocks[$subchunk] = $scriptblock; }
115 else if ($scriptblock > 0 && $scriptblock != $sbl) {
116 // *************************************************
117 // NEW (non-common) Script encountered in this chunk. Start a new subchunk
118 $subchunk++;
119 $scriptblock = $sbl;
120 $charctr = 0;
121 $scriptblocks[$subchunk] = $scriptblock;
122 }
123 }
124
125 $OTLdata[$subchunk][$charctr]['general_category'] = $ucd_record[0];
126 $OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
127
128 //$OTLdata[$subchunk][$charctr]['combining_class'] = $ucd_record[1];
129 //$OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
130 //$OTLdata[$subchunk][$charctr]['mirrored'] = $ucd_record[3];
131 //$OTLdata[$subchunk][$charctr]['east_asian_width'] = $ucd_record[4];
132 //$OTLdata[$subchunk][$charctr]['normalization_check'] = $ucd_record[5];
133 //$OTLdata[$subchunk][$charctr]['script'] = $ucd_record[6];
134
135 $charasstr = $this->unicode_hex($char);
136
137 if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $OTLdata[$subchunk][$charctr]['group'] = 'M'; }
138 else if ($char == 32 || $char == 12288) { $OTLdata[$subchunk][$charctr]['group'] = 'S'; } // 12288 = 0x3000 = CJK space
139 else { $OTLdata[$subchunk][$charctr]['group'] = 'C'; }
140
141 $OTLdata[$subchunk][$charctr]['uni'] = $char;
142 $OTLdata[$subchunk][$charctr]['hex'] = $charasstr;
143 $charctr++;
144 }
145
146 /* PROCESS EACH SUBCHUNK WITH DIFFERENT SCRIPTS */
147 for($sch=0;$sch<=$subchunk;$sch++) {
148 $this->OTLdata = $OTLdata[$sch];
149 $scriptblock = $scriptblocks[$sch];
150
151 // 3. Get Appropriate Scripts, and Shaper engine from analysing text and list of available scripts/langsys in font
152 //==============================
153 // Based on actual script block of text, select shaper (and line-breaking dictionaries)
154 if (UCDN::SCRIPT_DEVANAGARI <= $scriptblock && $scriptblock <= UCDN::SCRIPT_MALAYALAM) { $this->shaper = "I"; } // INDIC shaper
155 else if ($scriptblock == UCDN::SCRIPT_ARABIC || $scriptblock == UCDN::SCRIPT_SYRIAC) { $this->shaper = "A"; } // ARABIC shaper
156 else if ($scriptblock == UCDN::SCRIPT_NKO || $scriptblock == UCDN::SCRIPT_MANDAIC) { $this->shaper = "A"; } // ARABIC shaper
157 else if ($scriptblock == UCDN::SCRIPT_KHMER) { $this->shaper = "K"; } // KHMER shaper
158 else if ($scriptblock == UCDN::SCRIPT_THAI) { $this->shaper = "T"; } // THAI shaper
159 else if ($scriptblock == UCDN::SCRIPT_LAO) { $this->shaper = "L"; } // LAO shaper
160 else if ($scriptblock == UCDN::SCRIPT_SINHALA) { $this->shaper = "S"; } // SINHALA shaper
161 else if ($scriptblock == UCDN::SCRIPT_MYANMAR) { $this->shaper = "M"; } // MYANMAR shaper
162 else if ($scriptblock == UCDN::SCRIPT_NEW_TAI_LUE) { $this->shaper = "E"; } // SEA South East Asian shaper
163 else if ($scriptblock == UCDN::SCRIPT_CHAM) { $this->shaper = "E"; } // SEA South East Asian shaper
164 else if ($scriptblock == UCDN::SCRIPT_TAI_THAM) { $this->shaper = "E"; } // SEA South East Asian shaper
165 else $this->shaper = "";
166 // Get scripttag based on actual text script
167 $scripttag = UCDN::$uni_scriptblock[$scriptblock];
168
169 $GSUBscriptTag = '';
170 $GSUBlangsys = '';
171 $GPOSscriptTag = '';
172 $GPOSlangsys = '';
173 $is_old_spec = false;
174
175 $ScriptLang = $this->mpdf->CurrentFont['GSUBScriptLang'];
176 if (count($ScriptLang)) {
177 list($GSUBscriptTag,$is_old_spec) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GSUB');
178 if ($this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GSUBscriptTag], $this->mpdf->fontLanguageOverride)!==false) {
179 $GSUBlangsys = str_pad($this->mpdf->fontLanguageOverride,4);
180 }
181 else if ($GSUBscriptTag && isset($ScriptLang[$GSUBscriptTag]) && $ScriptLang[$GSUBscriptTag]!='') {
182 $GSUBlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GSUBscriptTag]);
183 }
184 }
185 $ScriptLang = $this->mpdf->CurrentFont['GPOSScriptLang'];
186
187 // NB If after GSUB, the same script/lang exist for GPOS, just use these...
188 if ($GSUBscriptTag && $GSUBlangsys && isset($ScriptLang[$GSUBscriptTag]) && strpos($ScriptLang[$GSUBscriptTag], $GSUBlangsys)!==false) {
189 $GPOSlangsys = $GSUBlangsys;
190 $GPOSscriptTag = $GSUBscriptTag;
191 }
192
193 // else repeat for GPOS
194 // [Font XBRiyaz has GSUB tables for latn, but not GPOS for latn]
195 else if (count($ScriptLang)) {
196 list($GPOSscriptTag,$dummy) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GPOS');
197 if ($GPOSscriptTag && $this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GPOSscriptTag], $this->mpdf->fontLanguageOverride)!==false) {
198 $GPOSlangsys = str_pad($this->mpdf->fontLanguageOverride,4);
199 }
200 else if ($GPOSscriptTag && isset($ScriptLang[$GPOSscriptTag]) && $ScriptLang[$GPOSscriptTag]!='') {
201 $GPOSlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GPOSscriptTag]);
202 }
203 }
204
205 ////////////////////////////////////////////////////////////////
206 // This is just for the font_dump_OTL utility to set script and langsys override
207 if (isset($this->mpdf->overrideOTLsettings) && isset($this->mpdf->overrideOTLsettings[$this->fontkey])) {
208 $GSUBscriptTag = $GPOSscriptTag = $this->mpdf->overrideOTLsettings[$this->fontkey]['script'];
209 $GSUBlangsys = $GPOSlangsys = $this->mpdf->overrideOTLsettings[$this->fontkey]['lang'];
210 }
211 ////////////////////////////////////////////////////////////////
212
213 if (!$GSUBscriptTag && !$GSUBlangsys && !$GPOSscriptTag && !$GPOSlangsys) {
214 // Remove ZWJ and ZWNJ
215 for ($i=0;$i<count($this->OTLdata);$i++) {
216 if ($this->OTLdata[$i]['uni']==8204 || $this->OTLdata[$i]['uni']==8205) {
217 array_splice($this->OTLdata, $i, 1);
218 }
219 }
220 $this->schOTLdata[$sch] = $this->OTLdata;
221 $this->OTLdata = array();
222 continue;
223 }
224
225 // Don't use MYANMAR shaper unless using v2 scripttag
226 if ($this->shaper == 'M' && $GSUBscriptTag != 'mym2') { $this->shaper = ''; }
227
228 $GSUBFeatures = (isset($this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys]) ? $this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys] : false);
229 $GPOSFeatures = (isset($this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys]) ? $this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys] : false);
230
231 $this->assocLigs = array(); // Ligatures[$posarr lpos] => nc
232 $this->assocMarks = array(); // assocMarks[$posarr mpos] => array(compID, ligPos)
233
234 if (!isset($this->GDEFdata[$this->fontkey]['GSUBGPOStables'])) {
235 $this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'] = file_get_contents(_MPDF_TTFONTDATAPATH.$this->fontkey.'.GSUBGPOStables.dat','rb') or die('Can\'t open file ' . _MPDF_TTFONTDATAPATH.$this->fontkey.'.GSUBGPOStables.dat');
236 }
237 else {
238 $this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'];
239 }
240
241
242 if ($this->debugOTL) { $this->_dumpproc('BEGIN', '-', '-', '-', '-', -1, '-', 0); }
243
244
245 ////////////////////////////////////////////////////////////////
246 ////////////////////////////////////////////////////////////////
247 ///////// LINE BREAKING FOR KHMER, THAI + LAO /////////////////
248 ////////////////////////////////////////////////////////////////
249 ////////////////////////////////////////////////////////////////
250 // Insert U+200B at word boundaries using dictionaries
251 if ($this->mpdf->useDictionaryLBR && ($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L")) {
252 // Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
253 $this->SEAlineBreaking();
254 }
255 // Insert U+200B at word boundaries for Tibetan
256 else if ($this->mpdf->useTibetanLBR && $scriptblock == UCDN::SCRIPT_TIBETAN ) {
257 // Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
258 $this->TibetanlineBreaking();
259 }
260 ////////////////////////////////////////////////////////////////
261 ////////////////////////////////////////////////////////////////
262 ////////// GSUB /////////////////////////////////
263 ////////////////////////////////////////////////////////////////
264 ////////////////////////////////////////////////////////////////
265 if (($useOTL & 0xFF) && $GSUBscriptTag && $GSUBlangsys && $GSUBFeatures) {
266
267 // 4. Load GSUB data, Coverage & Lookups
268 //=================================================================
269
270 $this->GSUBfont = $this->fontkey.'.GSUB.'.$GSUBscriptTag.'.'.$GSUBlangsys;
271
272 if (!isset($this->GSUBdata[$this->GSUBfont])) {
273 if (file_exists(_MPDF_TTFONTDATAPATH.$this->mpdf->CurrentFont['fontkey'].'.GSUB.'.$GSUBscriptTag.'.'.$GSUBlangsys.'.php')) {
274 include_once(_MPDF_TTFONTDATAPATH.$this->mpdf->CurrentFont['fontkey'].'.GSUB.'.$GSUBscriptTag.'.'.$GSUBlangsys.'.php');
275 $this->GSUBdata[$this->GSUBfont]['rtlSUB'] = $rtlSUB;
276 $this->GSUBdata[$this->GSUBfont]['finals'] = $finals;
277 if ($this->shaper=='I') {
278 $this->GSUBdata[$this->GSUBfont]['rphf'] = $rphf;
279 $this->GSUBdata[$this->GSUBfont]['half'] = $half;
280 $this->GSUBdata[$this->GSUBfont]['pref'] = $pref;
281 $this->GSUBdata[$this->GSUBfont]['blwf'] = $blwf;
282 $this->GSUBdata[$this->GSUBfont]['pstf'] = $pstf;
283 }
284 }
285 else { $this->GSUBdata[$this->GSUBfont] = array('rtlSUB'=>array(), 'rphf'=>array(), 'rphf'=>array(),
286 'pref'=>array(), 'blwf'=>array(), 'pstf'=>array(), 'finals'=>''
287 );
288 }
289 }
290
291 if (!isset($this->GSUBdata[$this->fontkey])) {
292 include(_MPDF_TTFONTDATAPATH.$this->fontkey.'.GSUBdata.php');
293 $this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'] = $GSLuCoverage;
294 }
295 else {
296 $this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'];
297 }
298
299 $this->GSUBLookups = $this->mpdf->CurrentFont['GSUBLookups'];
300
301
302 // 5(A). GSUB - Shaper - ARABIC
303 //==============================
304 if ($this->shaper == 'A') {
305 //-----------------------------------------------------------------------------------
306 // a. Apply initial GSUB Lookups (in order specified in lookup list but only selecting from certain tags)
307 //-----------------------------------------------------------------------------------
308 $tags = 'locl ccmp';
309 $omittags = '';
310 $usetags = $tags;
311 if(!empty($this->mpdf->OTLtags)) {
312 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true) ;
313 }
314 $this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
315
316 //-----------------------------------------------------------------------------------
317 // b. Apply context-specific forms GSUB Lookups (initial, isolated, medial, final)
318 //-----------------------------------------------------------------------------------
319 // Arab and Syriac are the only scripts requiring the special joining - which takes the place of
320 // isol fina medi init rules in GSUB (+ fin2 fin3 med2 in Syriac syrc)
321 $tags = 'isol fina fin2 fin3 medi med2 init';
322 $omittags = '';
323 $usetags = $tags;
324 if(!empty($this->mpdf->OTLtags)) {
325 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true) ;
326 }
327
328 $this->arabGlyphs = $this->GSUBdata[$this->GSUBfont]['rtlSUB'];
329
330 $gcms = explode("| ",$this->GlyphClassMarks);
331 $gcm = array();
332 foreach($gcms AS $g) { $gcm[hexdec($g)] = 1; }
333 $this->arabTransparentJoin = $this->arabTransparent + $gcm;
334 $this->arabic_shaper($usetags, $GSUBscriptTag);
335
336 //-----------------------------------------------------------------------------------
337 // c. Set Kashida points (after joining occurred - medi, fina, init) but before other substitutions
338 //-----------------------------------------------------------------------------------
339 //if ($scriptblock == UCDN::SCRIPT_ARABIC ) {
340 for ($i=0;$i<count($this->OTLdata);$i++) {
341 // Put the kashida marker on the character BEFORE which is inserted the kashida
342 // Kashida marker is inverse of priority i.e. Priority 1 => 7, Priority 7 => 1.
343
344 // Priority 1 User-inserted Kashida 0640 = Tatweel
345 // The user entered a Kashida in a position
346 // Position: Before the user-inserted kashida
347 if ($this->OTLdata[$i]['uni']==0x0640) {
348 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 8; // Put before the next character
349 }
350
351 // Priority 2 Seen (0633) FEB3, FEB4; Sad (0635) FEBB, FEBC
352 // Initial or medial form
353 // Connecting to the next character
354 // Position: After the character
355 else if ($this->OTLdata[$i]['uni']==0xFEB3 || $this->OTLdata[$i]['uni']==0xFEB4 || $this->OTLdata[$i]['uni']==0xFEBB || $this->OTLdata[$i]['uni']==0xFEBC) {
356 $checkpos = $i+1;
357 while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex'])!==false) {
358 $checkpos++;
359 }
360 if (isset($this->OTLdata[$checkpos])) {
361 $this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 7; // Put after marks on next character
362 }
363 }
364
365 // Priority 3 Taa Marbutah (0629) FE94; Haa (062D) FEA2; Dal (062F) FEAA
366 // Final form
367 // Connecting to previous character
368 // Position: Before the character
369 else if ($this->OTLdata[$i]['uni']==0xFE94 || $this->OTLdata[$i]['uni']==0xFEA2 || $this->OTLdata[$i]['uni']==0xFEAA) {
370 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 6;
371 }
372
373 // Priority 4 Alef (0627) FE8E; Tah (0637) FEC2; Lam (0644) FEDE; Kaf (0643) FEDA; Gaf (06AF) FB93
374 // Final form
375 // Connecting to previous character
376 // Position: Before the character
377 else if ($this->OTLdata[$i]['uni']==0xFE8E || $this->OTLdata[$i]['uni']==0xFEC2 || $this->OTLdata[$i]['uni']==0xFEDE || $this->OTLdata[$i]['uni']==0xFEDA || $this->OTLdata[$i]['uni']==0xFB93) {
378 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 5;
379 }
380
381 // Priority 5 RA (0631) FEAE; Ya (064A) FEF2 FEF4; Alef Maqsurah (0649) FEF0 FBE9
382 // Final or Medial form
383 // Connected to preceding medial BAA (0628) = FE92
384 // Position: Before preceding medial Baa
385 // Although not mentioned in spec, added Farsi Yeh (06CC) FBFD FBFF; equivalent to 064A or 0649
386 else if ($this->OTLdata[$i]['uni']==0xFEAE || $this->OTLdata[$i]['uni']==0xFEF2 || $this->OTLdata[$i]['uni']==0xFEF0
387 || $this->OTLdata[$i]['uni']==0xFEF4 || $this->OTLdata[$i]['uni']==0xFBE9
388 || $this->OTLdata[$i]['uni']==0xFBFD || $this->OTLdata[$i]['uni']==0xFBFF
389 ) {
390 $checkpos = $i-1;
391 while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex'])!==false) {
392 $checkpos--;
393 }
394 if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni']==0xFE92) {
395 $this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 4; // ******* Before preceding BAA
396 }
397 }
398
399 // Priority 6 WAW (0648) FEEE; Ain (0639) FECA; Qaf (0642) FED6; Fa (0641) FED2
400 // Final form
401 // Connecting to previous character
402 // Position: Before the character
403 else if ($this->OTLdata[$i]['uni']==0xFEEE || $this->OTLdata[$i]['uni']==0xFECA || $this->OTLdata[$i]['uni']==0xFED6 || $this->OTLdata[$i]['uni']==0xFED2) {
404 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 3;
405 }
406
407 // Priority 7 Other connecting characters
408 // Final form
409 // Connecting to previous character
410 // Position: Before the character
411 /* This isn't in the spec, but using MS WORD as a basis, give a lower priority to the 3 characters already checked
412 in (5) above. Test case:
413 &#x62e;&#x652;&#x631;&#x64e;&#x649;&#x670;
414 &#x641;&#x64e;&#x62a;&#x64f;&#x630;&#x64e;&#x643;&#x651;&#x650;&#x631;
415 */
416
417 if (!isset($this->OTLdata[$i]['GPOSinfo']['kashida'])) {
418 if (strpos($this->GSUBdata[$this->GSUBfont]['finals'], $this->OTLdata[$i]['hex'])!==false) { // ANY OTHER FINAL FORM
419 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 2;
420 }
421 else if (strpos('0FEAE 0FEF0 0FEF2',$this->OTLdata[$i]['hex'])!==false) { // not already included in 5 above
422 $this->OTLdata[$i]['GPOSinfo']['kashida'] = 1;
423 }
424 }
425 }
426
427 //-----------------------------------------------------------------------------------
428 // d. Apply Presentation Forms GSUB Lookups (+ any discretionary) - Apply one at a time in Feature order
429 //-----------------------------------------------------------------------------------
430 $tags = 'rlig calt liga clig mset';
431
432 $omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
433 $usetags = $tags;
434 if(!empty($this->mpdf->OTLtags)) {
435 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false) ;
436 }
437
438 $ts = explode(' ',$usetags);
439 foreach($ts AS $ut) { // - Apply one at a time in Feature order
440 $this->_applyGSUBrules($ut, $GSUBscriptTag, $GSUBlangsys);
441 }
442 //-----------------------------------------------------------------------------------
443 // e. NOT IN SPEC
444 // If space precedes a mark -> substitute a &nbsp; before the Mark, to prevent line breaking Test:
445 //-----------------------------------------------------------------------------------
446 for($ptr=1; $ptr<count($this->OTLdata); $ptr++) {
447 if ($this->OTLdata[$ptr]['general_category'] == UCDN::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK && $this->OTLdata[$ptr-1]['uni'] == 32) {
448 $this->OTLdata[$ptr-1]['uni'] = 0xa0;
449 $this->OTLdata[$ptr-1]['hex'] = '000A0';
450 }
451 }
452 }
453
454 // 5(I). GSUB - Shaper - INDIC and SINHALA and KHMER
455 //===================================
456 else if ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
457 $this->restrictToSyllable = true;
458 //-----------------------------------------------------------------------------------
459 // a. First decompose/compose split mattras
460 // (normalize) ??????? Nukta/Halant order etc ??????????????????????????????????????????????????????????????????????????
461 //-----------------------------------------------------------------------------------
462 for($ptr=0; $ptr<count($this->OTLdata); $ptr++) {
463 $char = $this->OTLdata[$ptr]['uni'];
464 $sub = INDIC::decompose_indic($char);
465 if ($sub) {
466 $newinfo = array();
467 for($i=0;$i<count($sub);$i++) {
468 $newinfo[$i] = array();
469 $ucd_record = UCDN::get_ucd_record($sub[$i]);
470 $newinfo[$i]['general_category'] = $ucd_record[0];
471 $newinfo[$i]['bidi_type'] = $ucd_record[2];
472 $charasstr = $this->unicode_hex($sub[$i]);
473 if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[$i]['group'] = 'M'; }
474 else { $newinfo[$i]['group'] = 'C'; }
475 $newinfo[$i]['uni'] = $sub[$i];
476 $newinfo[$i]['hex'] = $charasstr;
477 }
478 array_splice($this->OTLdata, $ptr, 1, $newinfo);
479 $ptr += count($sub)-1;
480 }
481 /* Only Composition-exclusion exceptions that we want to recompose. */
482 if ($this->shaper == 'I') {
483 if ($char == 0x09AF && isset($this->OTLdata[$ptr + 1]) && $this->OTLdata[$ptr + 1]['uni'] == 0x09BC) {
484 $sub = 0x09DF;
485 $newinfo = array();
486 $newinfo[0] = array();
487 $ucd_record = UCDN::get_ucd_record($sub);
488 $newinfo[0]['general_category'] = $ucd_record[0];
489 $newinfo[0]['bidi_type'] = $ucd_record[2];
490 $newinfo[0]['group'] = 'C';
491 $newinfo[0]['uni'] = $sub;
492 $newinfo[0]['hex'] = $this->unicode_hex($sub);
493 array_splice($this->OTLdata, $ptr, 2, $newinfo);
494 }
495 }
496 }
497 //-----------------------------------------------------------------------------------
498 // b. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
499 //-----------------------------------------------------------------------------------
500 $indic_category_string = '';
501 foreach($this->OTLdata AS $eid=>$c) {
502 INDIC::set_indic_properties($this->OTLdata[$eid], $scriptblock ); // sets ['indic_category'] and ['indic_position']
503 //$c['general_category']
504 //$c['combining_class']
505 //$c['uni'] = $char;
506
507 $indic_category_string .= INDIC::$indic_category_char[$this->OTLdata[$eid]['indic_category']];
508 }
509
510 $broken_syllables = false;
511 if ($this->shaper == 'I') {
512 INDIC::set_syllables($this->OTLdata, $indic_category_string, $broken_syllables);
513 }
514 else if ($this->shaper == 'S') {
515 INDIC::set_syllables_sinhala($this->OTLdata, $indic_category_string, $broken_syllables);
516 }
517 else if ($this->shaper == 'K') {
518 INDIC::set_syllables_khmer($this->OTLdata, $indic_category_string, $broken_syllables);
519 }
520 $indic_category_string = '';
521
522 //-----------------------------------------------------------------------------------
523 // c. Initial Re-ordering (Indic / Khmer / Sinhala)
524 //-----------------------------------------------------------------------------------
525 // Find base consonant
526 // Decompose/compose and reorder Matras
527 // Reorder marks to canonical order
528
529 $indic_config = INDIC::$indic_configs[$scriptblock];
530 $dottedcircle = false;
531 if ($broken_syllables) {
532 if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'],0x25CC) ) {
533 $dottedcircle = array();
534 $ucd_record = UCDN::get_ucd_record(0x25CC);
535 $dottedcircle[0]['general_category'] = $ucd_record[0];
536 $dottedcircle[0]['bidi_type'] = $ucd_record[2];
537 $dottedcircle[0]['group'] = 'C';
538 $dottedcircle[0]['uni'] = 0x25CC;
539 $dottedcircle[0]['indic_category'] = INDIC::OT_DOTTEDCIRCLE;
540 $dottedcircle[0]['indic_position'] = INDIC::POS_BASE_C;
541
542 $dottedcircle[0]['hex'] = '025CC'; // TEMPORARY *****
543 }
544 }
545 INDIC::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $indic_config, $scriptblock, $is_old_spec, $dottedcircle);
546
547 //-----------------------------------------------------------------------------------
548 // d. Apply initial and basic shaping forms GSUB Lookups (one at a time)
549 //-----------------------------------------------------------------------------------
550 if ($this->shaper == 'I' || $this->shaper == 'S') {
551 $tags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct';
552 }
553 else if ($this->shaper == 'K') {
554 $tags = 'locl ccmp pref blwf abvf pstf cfar';
555 }
556 $this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
557
558 //-----------------------------------------------------------------------------------
559 // e. Final Re-ordering (Indic / Khmer / Sinhala)
560 //-----------------------------------------------------------------------------------
561 // Reorder matras
562 // Reorder reph
563 // Reorder pre-base reordering consonants:
564
565 INDIC::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $indic_config, $scriptblock, $is_old_spec);
566
567 //-----------------------------------------------------------------------------------
568 // f. Apply 'init' feature to first syllable in word (indicated by ['mask']) INDIC::FLAG(INDIC::INIT);
569 //-----------------------------------------------------------------------------------
570 if ($this->shaper == 'I' || $this->shaper == 'S') {
571 $tags = 'init';
572 $this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
573 }
574
575 //-----------------------------------------------------------------------------------
576 // g. Apply Presentation Forms GSUB Lookups (+ any discretionary)
577 //-----------------------------------------------------------------------------------
578 $tags = 'pres abvs blws psts haln rlig calt liga clig mset';
579
580 $omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
581 $usetags = $tags;
582 if(!empty($this->mpdf->OTLtags)) {
583 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false) ;
584 }
585 if ($this->shaper == 'K') { // Features are applied one at a time, working through each codepoint
586 $this->_applyGSUBrulesSingly($usetags, $GSUBscriptTag, $GSUBlangsys);
587 }
588 else {
589 $this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
590 }
591 $this->restrictToSyllable = false;
592 }
593
594
595 // 5(M). GSUB - Shaper - MYANMAR (ONLY mym2)
596 //==============================
597 // NB Old style 'mymr' is left to go through the default shaper
598 else if ($this->shaper == 'M') {
599 $this->restrictToSyllable = true;
600 //-----------------------------------------------------------------------------------
601 // a. Analyse characters - group as syllables/clusters (Myanmar); invalid diacritics; add dotted circle
602 //-----------------------------------------------------------------------------------
603 $myanmar_category_string = '';
604 foreach($this->OTLdata AS $eid=>$c) {
605 MYANMAR::set_myanmar_properties($this->OTLdata[$eid]); // sets ['myanmar_category'] and ['myanmar_position']
606 $myanmar_category_string .= MYANMAR::$myanmar_category_char[$this->OTLdata[$eid]['myanmar_category']];
607 }
608 $broken_syllables = false;
609 MYANMAR::set_syllables($this->OTLdata, $myanmar_category_string, $broken_syllables);
610 $myanmar_category_string = '';
611
612 //-----------------------------------------------------------------------------------
613 // b. Re-ordering (Myanmar mym2)
614 //-----------------------------------------------------------------------------------
615 $dottedcircle = false;
616 if ($broken_syllables) {
617 if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'],0x25CC) ) {
618 $dottedcircle = array();
619 $ucd_record = UCDN::get_ucd_record(0x25CC);
620 $dottedcircle[0]['general_category'] = $ucd_record[0];
621 $dottedcircle[0]['bidi_type'] = $ucd_record[2];
622 $dottedcircle[0]['group'] = 'C';
623 $dottedcircle[0]['uni'] = 0x25CC;
624 $dottedcircle[0]['myanmar_category'] = MYANMAR::OT_DOTTEDCIRCLE;
625 $dottedcircle[0]['myanmar_position'] = MYANMAR::POS_BASE_C;
626 $dottedcircle[0]['hex'] = '025CC';
627 }
628 }
629 MYANMAR::reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $dottedcircle);
630
631 //-----------------------------------------------------------------------------------
632 // c. Apply initial and basic shaping forms GSUB Lookups (one at a time)
633 //-----------------------------------------------------------------------------------
634
635 $tags = 'locl ccmp rphf pref blwf pstf';
636 $this->_applyGSUBrulesMyanmar($tags, $GSUBscriptTag, $GSUBlangsys);
637
638 //-----------------------------------------------------------------------------------
639 // d. Apply Presentation Forms GSUB Lookups (+ any discretionary)
640 //-----------------------------------------------------------------------------------
641 $tags = 'pres abvs blws psts haln rlig calt liga clig mset';
642 $omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
643 $usetags = $tags;
644 if(!empty($this->mpdf->OTLtags)) {
645 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false) ;
646 }
647 $this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
648 $this->restrictToSyllable = false;
649 }
650
651
652 // 5(E). GSUB - Shaper - SEA South East Asian (New Tai Lue, Cham, Tai Tam)
653 //==============================
654 else if ($this->shaper == 'E') {
655 /* HarfBuzz says: If the designer designed the font for the 'DFLT' script,
656 * use the default shaper. Otherwise, use the SEA shaper.
657 * Note that for some simple scripts, there may not be *any*
658 * GSUB/GPOS needed, so there may be no scripts found! */
659
660 $this->restrictToSyllable = true;
661 //-----------------------------------------------------------------------------------
662 // a. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
663 //-----------------------------------------------------------------------------------
664 $sea_category_string = '';
665 foreach($this->OTLdata AS $eid=>$c) {
666 SEA::set_sea_properties($this->OTLdata[$eid], $scriptblock ); // sets ['sea_category'] and ['sea_position']
667 //$c['general_category']
668 //$c['combining_class']
669 //$c['uni'] = $char;
670
671 $sea_category_string .= SEA::$sea_category_char[$this->OTLdata[$eid]['sea_category']];
672 }
673
674 $broken_syllables = false;
675 SEA::set_syllables($this->OTLdata, $sea_category_string, $broken_syllables);
676 $sea_category_string = '';
677
678 //-----------------------------------------------------------------------------------
679 // b. Apply locl and ccmp shaping forms - before initial re-ordering; GSUB Lookups (one at a time)
680 //-----------------------------------------------------------------------------------
681 $tags = 'locl ccmp';
682 $this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
683
684 //-----------------------------------------------------------------------------------
685 // c. Initial Re-ordering
686 //-----------------------------------------------------------------------------------
687 // Find base consonant
688 // Decompose/compose and reorder Matras
689 // Reorder marks to canonical order
690
691 $dottedcircle = false;
692 if ($broken_syllables) {
693 if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'],0x25CC) ) {
694 $dottedcircle = array();
695 $ucd_record = UCDN::get_ucd_record(0x25CC);
696 $dottedcircle[0]['general_category'] = $ucd_record[0];
697 $dottedcircle[0]['bidi_type'] = $ucd_record[2];
698 $dottedcircle[0]['group'] = 'C';
699 $dottedcircle[0]['uni'] = 0x25CC;
700 $dottedcircle[0]['sea_category'] = SEA::OT_GB;
701 $dottedcircle[0]['sea_position'] = SEA::POS_BASE_C;
702
703 $dottedcircle[0]['hex'] = '025CC'; // TEMPORARY *****
704 }
705 }
706 SEA::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $scriptblock, $dottedcircle);
707
708 //-----------------------------------------------------------------------------------
709 // d. Apply basic shaping forms GSUB Lookups (one at a time)
710 //-----------------------------------------------------------------------------------
711 $tags = 'pref abvf blwf pstf';
712 $this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
713
714 //-----------------------------------------------------------------------------------
715 // e. Final Re-ordering
716 //-----------------------------------------------------------------------------------
717
718 SEA::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $scriptblock);
719
720 //-----------------------------------------------------------------------------------
721 // f. Apply Presentation Forms GSUB Lookups (+ any discretionary)
722 //-----------------------------------------------------------------------------------
723 $tags = 'pres abvs blws psts';
724
725 $omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
726 $usetags = $tags;
727 if(!empty($this->mpdf->OTLtags)) {
728 $usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false) ;
729 }
730 $this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
731 $this->restrictToSyllable = false;
732 }
733
734
735 // 5(D). GSUB - Shaper - DEFAULT (including THAI and LAO and MYANMAR v1 [mymr] and TIBETAN)
736 //==============================
737 else { // DEFAULT
738 //-----------------------------------------------------------------------------------
739 // a. First decompose/compose in Thai / Lao - Tibetan
740 //-----------------------------------------------------------------------------------
741 // Decomposition for THAI or LAO
742 /* This function implements the shaping logic documented here:
743 *
744 * http://linux.thai.net/~thep/th-otf/shaping.html
745 *
746 * The first shaping rule listed there is needed even if the font has Thai
747 * OpenType tables.
748 *
749 *
750 * The following is NOT specified in the MS OT Thai spec, however, it seems
751 * to be what Uniscribe and other engines implement. According to Eric Muller:
752 *
753 * When you have a SARA AM, decompose it in NIKHAHIT + SARA AA, *and* move the
754 * NIKHAHIT backwards over any tone mark (0E48-0E4B).
755 *
756 * <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32>
757 *
758 * This reordering is legit only when the NIKHAHIT comes from a SARA AM, not
759 * when it's there to start with. The string <0E14, 0E4B, 0E4D> is probably
760 * not what a user wanted, but the rendering is nevertheless nikhahit above
761 * chattawa.
762 *
763 * Same for Lao.
764 *
765 * Thai Lao
766 * SARA AM: U+0E33 U+0EB3
767 * SARA AA: U+0E32 U+0EB2
768 * Nikhahit: U+0E4D U+0ECD
769 *
770 * Testing shows that Uniscribe reorder the following marks:
771 * Thai: <0E31,0E34..0E37,0E47..0E4E>
772 * Lao: <0EB1,0EB4..0EB7,0EC7..0ECE>
773 *
774 * Lao versions are the same as Thai + 0x80.
775 */
776 if ($this->shaper == 'T' || $this->shaper == 'L') {
777 for($ptr=0; $ptr<count($this->OTLdata); $ptr++) {
778 $char = $this->OTLdata[$ptr]['uni'];
779 if (($char & ~0x0080) == 0x0E33) { // if SARA_AM (U+0E33 or U+0EB3)
780
781 $NIKHAHIT = $char + 0x1A;
782 $SARA_AA = $char - 1;
783 $sub = array($SARA_AA, $NIKHAHIT);
784
785 $newinfo = array();
786 $ucd_record = UCDN::get_ucd_record($sub[0]);
787 $newinfo[0]['general_category'] = $ucd_record[0];
788 $newinfo[0]['bidi_type'] = $ucd_record[2];
789 $charasstr = $this->unicode_hex($sub[0]);
790 if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[0]['group'] = 'M'; }
791 else { $newinfo[0]['group'] = 'C'; }
792 $newinfo[0]['uni'] = $sub[0];
793 $newinfo[0]['hex'] = $charasstr;
794 $this->OTLdata[$ptr] = $newinfo[0]; // Substitute SARA_AM => SARA_AA
795
796 $ntones = 0; // number of (preceding) tone marks
797 // IS_TONE_MARK ((x) & ~0x0080, 0x0E34 - 0x0E37, 0x0E47 - 0x0E4E, 0x0E31)
798 while (isset($this->OTLdata[$ptr - 1 - $ntones])
799 && (
800 ($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) == 0x0E31 ||
801
802 (($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E34 &&
803 ($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E37) ||
804
805 (($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E47 &&
806 ($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E4E)
807 )
808 ) { $ntones++; }
809
810 $newinfo = array();
811 $ucd_record = UCDN::get_ucd_record($sub[1]);
812 $newinfo[0]['general_category'] = $ucd_record[0];
813 $newinfo[0]['bidi_type'] = $ucd_record[2];
814 $charasstr = $this->unicode_hex($sub[1]);
815 if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[0]['group'] = 'M'; }
816 else { $newinfo[0]['group'] = 'C'; }
817 $newinfo[0]['uni'] = $sub[1];
818 $newinfo[0]['hex'] = $charasstr;
819 // Insert NIKAHIT
820 array_splice($this->OTLdata, $ptr - $ntones, 0, $newinfo);
821
822 $ptr++;
823 }
824 }
825 }
826
827 if ($scriptblock == UCDN::SCRIPT_TIBETAN) {
828 // =========================
829 // Reordering TIBETAN
830 // =========================
831 // Tibetan does not need to need a shaper generally, as long as characters are presented in the correct order
832 // so we will do one minor change here:
833 // From ICU: If the present character is a number, and the next character is a pre-number combining mark
834 // then the two characters are reordered
835 // From MS OTL spec the following are Digit modifiers (Md): 0F18�0F19, 0F3E�0F3F
836 // Digits: 0F20�0F33
837 // On testing only 0x0F3F (pre-based mark) seems to need re-ordering
838 for($ptr=0; $ptr<count($this->OTLdata)-1; $ptr++) {
839 if (INDIC::in_range($this->OTLdata[$ptr]['uni'], 0x0F20, 0x0F33) && $this->OTLdata[$ptr+1]['uni'] == 0x0F3F ) {
840 $tmp = $this->OTLdata[$ptr+1];
841 $this->OTLdata[$ptr+1] = $this->OTLdata[$ptr];
842 $this->OTLdata[$ptr] = $tmp;
843 }
844 }
845
846
847 // =========================
848 // Decomposition for TIBETAN
849 // =========================
850 /* Recommended, but does not seem to change anything...
851 for($ptr=0; $ptr<count($this->OTLdata); $ptr++) {
852 $char = $this->OTLdata[$ptr]['uni'];
853 $sub = INDIC::decompose_indic($char);
854 if ($sub) {
855 $newinfo = array();
856 for($i=0;$i<count($sub);$i++) {
857 $newinfo[$i] = array();
858 $ucd_record = UCDN::get_ucd_record($sub[$i]);
859 $newinfo[$i]['general_category'] = $ucd_record[0];
860 $newinfo[$i]['bidi_type'] = $ucd_record[2];
861 $charasstr = $this->unicode_hex($sub[$i]);
862 if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[$i]['group'] = 'M'; }
863 else { $newinfo[$i]['group'] = 'C'; }
864 $newinfo[$i]['uni'] = $sub[$i];
865 $newinfo[$i]['hex'] = $charasstr;
866 }
867 array_splice($this->OTLdata, $ptr, 1, $newinfo);
868 $ptr += count($sub)-1;
869 }
870 }
871 */
872
873 }
874
875
876 //-----------------------------------------------------------------------------------
877 // b. Apply all GSUB Lookups (in order specified in lookup list)
878 //-----------------------------------------------------------------------------------
879 $tags = 'locl ccmp pref blwf abvf pstf pres abvs blws psts haln rlig calt liga clig mset RQD';
880 // pref blwf abvf pstf required for Tibetan
881 // " RQD" is a non-standard tag in Garuda font - presumably intended to be used by default ? "ReQuireD"
882 // Being a 3 letter tag is non-standard, and does not allow it to be set by font-feature-settings
883
884
885 /* ?Add these until shapers witten?
886 Hangul: ljmo vjmo tjmo
887 */
888
889 $omittags = '';
890 $useGSUBtags = $tags;
891 if(!empty($this->mpdf->OTLtags)) {
892 $useGSUBtags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false) ;
893 }
894 // APPLY GSUB rules (as long as not Latin + SmallCaps - but not OTL smcp)
895 if (!(($this->mpdf->textvar & FC_SMALLCAPS) && $scriptblock == UCDN::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp')===false)) {
896 $this->_applyGSUBrules($useGSUBtags, $GSUBscriptTag, $GSUBlangsys);
897 }
898 }
899
900
901 }
902
903 // Shapers - KHMER & THAI & LAO - Replace Word boundary marker with U+200B
904 // Also TIBETAN (no shaper)
905 //=======================================================
906 if (($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L") || $scriptblock == UCDN::SCRIPT_TIBETAN ) {
907 // Set up properties to insert a U+200B character
908 $newinfo = array();
909 //$newinfo[0] = array('general_category' => 1, 'bidi_type' => 14, 'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B');
910 $newinfo[0] = array(
911 'general_category' => UCDN::UNICODE_GENERAL_CATEGORY_FORMAT,
912 'bidi_type' => UCDN::BIDI_CLASS_BN,
913 'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B');
914 // Then insert U+200B at (after) all word end boundaries
915 for ($i=count($this->OTLdata)-1;$i>0;$i--) {
916 // Make sure after GSUB that wordend has not been moved - check next char is not in the same syllable
917 if (isset($this->OTLdata[$i]['wordend']) && $this->OTLdata[$i]['wordend'] &&
918 isset($this->OTLdata[$i+1]['uni']) && (!isset($this->OTLdata[$i+1]['syllable']) || !isset($this->OTLdata[$i+1]['syllable']) || $this->OTLdata[$i+1]['syllable']!=$this->OTLdata[$i]['syllable'])) {
919 array_splice($this->OTLdata, $i+1, 0, $newinfo);
920 $this->_updateLigatureMarks($i, 1);
921 }
922 else if ($this->OTLdata[$i]['uni']==0x2e) { // Word end if Full-stop.
923 array_splice($this->OTLdata, $i+1, 0, $newinfo);
924 $this->_updateLigatureMarks($i, 1);
925 }
926 }
927 }
928
929
930 // Shapers - INDIC & ARABIC & KHMER & SINHALA & MYANMAR - Remove ZWJ and ZWNJ
931 //=======================================================
932 if ($this->shaper == 'I' || $this->shaper == 'S' || $this->shaper == 'A' || $this->shaper == 'K' || $this->shaper == 'M') {
933 // Remove ZWJ and ZWNJ
934 for ($i=0;$i<count($this->OTLdata);$i++) {
935 if ($this->OTLdata[$i]['uni']==8204 || $this->OTLdata[$i]['uni']==8205) {
936 array_splice($this->OTLdata, $i, 1);
937 $this->_updateLigatureMarks($i, -1);
938 }
939 }
940 }
941
942 //print_r($this->OTLdata); echo '<br />';
943 //print_r($this->assocMarks); echo '<br />';
944 //print_r($this->assocLigs); exit;
945
946 ////////////////////////////////////////////////////////////////
947 ////////////////////////////////////////////////////////////////
948 ////////// GPOS /////////////////////////////////
949 ////////////////////////////////////////////////////////////////
950 ////////////////////////////////////////////////////////////////
951
952 if (($useOTL & 0xFF) && $GPOSscriptTag && $GPOSlangsys && $GPOSFeatures) {
953 $this->Entry = array();
954 $this->Exit = array();
955
956 // 6. Load GPOS data, Coverage & Lookups
957 //=================================================================
958 if (!isset($this->GPOSdata[$this->fontkey])) {
959 include(_MPDF_TTFONTDATAPATH.$this->mpdf->CurrentFont['fontkey'].'.GPOSdata.php');
960 $this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'] = $LuCoverage;
961 }
962 else {
963 $this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'];
964 }
965
966 $this->GPOSLookups = $this->mpdf->CurrentFont['GPOSLookups'];
967
968
969 // 7. Select Feature tags to use (incl optional)
970 //==============================
971 $tags = 'abvm blwm mark mkmk curs cpsp dist requ'; // Default set
972 /* 'requ' is not listed in the Microsoft registry of Feature tags
973 Found in Arial Unicode MS, it repositions the baseline for punctuation in Kannada script */
974
975 // ZZZ96
976 // Set kern to be included by default in non-Latin script (? just when shapers used)
977 // Kern is used in some fonts to reposition marks etc. and is essential for correct display
978 //if ($this->shaper) {$tags .= ' kern'; }
979 if ($scriptblock != UCDN::SCRIPT_LATIN) { $tags .= ' kern'; }
980
981 $omittags = '';
982 $usetags = $tags;
983 if(!empty($this->mpdf->OTLtags)) {
984 $usetags = $this->_applyTagSettings($tags, $GPOSFeatures, $omittags, false) ;
985 }
986
987
988
989 // 8. Get GPOS LookupList from Feature tags
990 //==============================
991 $LookupList = array();
992 foreach($GPOSFeatures AS $tag=>$arr) {
993 if (strpos($usetags, $tag)!==false) {
994 foreach($arr AS $lu) { $LookupList[$lu] = $tag; }
995 }
996 }
997 ksort($LookupList);
998
999
1000 // 9. Apply GPOS Lookups (in order specified in lookup list but selecting from specified tags)
1001 //==============================
1002
1003 // APPLY THE GPOS RULES (as long as not Latin + SmallCaps - but not OTL smcp)
1004 if (!(($this->mpdf->textvar & FC_SMALLCAPS) && $scriptblock == UCDN::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp')===false)) {
1005 $this->_applyGPOSrules($LookupList, $is_old_spec);
1006 // (sets: $this->OTLdata[n]['GPOSinfo'] XPlacement YPlacement XAdvance Entry Exit )
1007 }
1008
1009 // 10. Process cursive text
1010 //==============================
1011 if (count($this->Entry) || count($this->Exit)) {
1012 // RTL
1013 $incurs = false;
1014 for ($i=(count($this->OTLdata)-1);$i>=0;$i--) {
1015 if (isset($this->Entry[$i]) && isset($this->Entry[$i]['Y']) && $this->Entry[$i]['dir']=='RTL') {
1016 $nextbase = $i-1; // Set as next base ignoring marks (next base reading RTL in logical oder
1017 while(isset($this->OTLdata[$nextbase]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex'])!==false) { $nextbase--; }
1018 if (isset($this->Exit[$nextbase]) && isset($this->Exit[$nextbase]['Y']) ) {
1019 $diff = $this->Entry[$i]['Y'] - $this->Exit[$nextbase]['Y'];
1020 if ($incurs===false) { $incurs = $diff; }
1021 else { $incurs += $diff; }
1022 for ($j=($i-1);$j>=$nextbase;$j--) {
1023 if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) { $this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs; }
1024 else { $this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs; }
1025 }
1026 if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X']) ) {
1027 $adj = -($this->Entry[$i]['X'] - $this->Exit[$nextbase]['X']);
1028 // If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1029 // in RTL - the current glyph or the last of any associated marks
1030 if (isset($this->OTLdata[$nextbase+1]['GPOSinfo']['XAdvance'])) { $this->OTLdata[$nextbase+1]['GPOSinfo']['XAdvance'] += $adj; }
1031 else { $this->OTLdata[$nextbase+1]['GPOSinfo']['XAdvance'] = $adj; }
1032 }
1033 }
1034 else { $incurs = false; }
1035 }
1036 else if (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex'])!==false) { continue; } // ignore Marks
1037 else { $incurs = false; }
1038 }
1039 // LTR
1040 $incurs = false;
1041 for ($i=0;$i<count($this->OTLdata);$i++) {
1042 if (isset($this->Exit[$i]) && isset($this->Exit[$i]['Y']) && $this->Exit[$i]['dir']=='LTR') {
1043 $nextbase = $i+1; // Set as next base ignoring marks
1044 while(strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex'])!==false) { $nextbase++; }
1045 if (isset($this->Entry[$nextbase]) && isset($this->Entry[$nextbase]['Y']) ) {
1046
1047 $diff = $this->Exit[$i]['Y'] - $this->Entry[$nextbase]['Y'];
1048 if ($incurs===false) { $incurs = $diff; }
1049 else { $incurs += $diff; }
1050 for ($j=($i+1);$j<=$nextbase;$j++) {
1051 if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) { $this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs; }
1052 else { $this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs; }
1053 }
1054 if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X']) ) {
1055 $adj = -($this->Exit[$i]['X'] - $this->Entry[$nextbase]['X']);
1056 // If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1057 // in LTR - the next glyph, ignoring marks
1058 if (isset($this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'])) { $this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] += $adj; }
1059 else { $this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] = $adj; }
1060 }
1061 }
1062 else { $incurs = false; }
1063 }
1064 else if (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex'])!==false) { continue; } // ignore Marks
1065 else { $incurs = false; }
1066 }
1067 }
1068
1069
1070
1071
1072 } // end GPOS
1073
1074 if ($this->debugOTL) { $this->_dumpproc('END', '-', '-', '-', '-', 0, '-', 0); exit; }
1075
1076 $this->schOTLdata[$sch] = $this->OTLdata;
1077 $this->OTLdata = array();
1078 } // END foreach subchunk
1079
1080
1081 // 11. Re-assemble and return text string
1082 //==============================
1083 $newGPOSinfo = array();
1084 $newOTLdata = array();
1085 $newchar_data = array();
1086 $newgroup = '';
1087 $e = '';
1088 $ectr = 0;
1089
1090 for($sch=0;$sch<=$subchunk;$sch++) {
1091 for ($i=0;$i<count($this->schOTLdata[$sch]);$i++) {
1092 if (isset($this->schOTLdata[$sch][$i]['GPOSinfo'])) {
1093 $newGPOSinfo[$ectr] = $this->schOTLdata[$sch][$i]['GPOSinfo'];
1094 }
1095 $newchar_data[$ectr] = array('bidi_class' => $this->schOTLdata[$sch][$i]['bidi_type'], 'uni' => $this->schOTLdata[$sch][$i]['uni']);
1096 $newgroup .= $this->schOTLdata[$sch][$i]['group'];
1097 $e.=code2utf($this->schOTLdata[$sch][$i]['uni']);
1098 if (isset($this->mpdf->CurrentFont['subset'])) {
1099 $this->mpdf->CurrentFont['subset'][$this->schOTLdata[$sch][$i]['uni']] = $this->schOTLdata[$sch][$i]['uni'];
1100 }
1101 $ectr++;
1102 }
1103
1104 }
1105 $this->OTLdata['GPOSinfo'] = $newGPOSinfo;
1106 $this->OTLdata['char_data'] = $newchar_data ;
1107 $this->OTLdata['group'] = $newgroup ;
1108
1109
1110 // This leaves OTLdata::GPOSinfo, ::bidi_type, & ::group
1111
1112 return $e;
1113
1114 }
1115
1116 function _applyTagSettings($tags, $Features, $omittags='', $onlytags=false) {
1117 if (empty($this->mpdf->OTLtags['Plus']) && empty($this->mpdf->OTLtags['Minus']) && empty($this->mpdf->OTLtags['FFPlus']) && empty($this->mpdf->OTLtags['FFMinus'])) { return $tags; }
1118
1119 // Use $tags as starting point
1120 $usetags = $tags;
1121
1122 // Only set / unset tags which are in the font
1123 // Ignore tags which are in $omittags
1124 // If $onlytags, then just unset tags which are already in the Tag list
1125
1126 $fp = $fm = $ffp = $ffm = '';
1127
1128 // Font features to enable - set by font-variant-xx
1129 if (isset($this->mpdf->OTLtags['Plus'])) $fp = $this->mpdf->OTLtags['Plus'];
1130 preg_match_all('/([a-zA-Z0-9]{4})/',$fp,$m);
1131 for($i=0;$i<count($m[0]);$i++) {
1132 $t = $m[1][$i];
1133 // Is it a valid tag?
1134 if(isset($Features[$t]) && strpos($omittags,$t)===false && (!$onlytags || strpos($tags,$t)!==false )) {
1135 $usetags .= ' '.$t;
1136 }
1137 }
1138
1139 // Font features to disable - set by font-variant-xx
1140 if (isset($this->mpdf->OTLtags['Minus'])) $fm = $this->mpdf->OTLtags['Minus'];
1141 preg_match_all('/([a-zA-Z0-9]{4})/',$fm,$m);
1142 for($i=0;$i<count($m[0]);$i++) {
1143 $t = $m[1][$i];
1144 // Is it a valid tag?
1145 if(isset($Features[$t]) && strpos($omittags,$t)===false && (!$onlytags || strpos($tags,$t)!==false )) {
1146 $usetags = str_replace($t,'',$usetags);
1147 }
1148 }
1149
1150 // Font features to enable - set by font-feature-settings
1151 if (isset($this->mpdf->OTLtags['FFPlus'])) $ffp = $this->mpdf->OTLtags['FFPlus']; // Font Features - may include integer: salt4
1152 preg_match_all('/([a-zA-Z0-9]{4})([\d+]*)/',$ffp,$m);
1153 for($i=0;$i<count($m[0]);$i++) {
1154 $t = $m[1][$i];
1155 // Is it a valid tag?
1156 if(isset($Features[$t]) && strpos($omittags,$t)===false && (!$onlytags || strpos($tags,$t)!==false )) {
1157 $usetags .= ' '.$m[0][$i]; // - may include integer: salt4
1158 }
1159 }
1160
1161 // Font features to disable - set by font-feature-settings
1162 if (isset($this->mpdf->OTLtags['FFMinus'])) $ffm = $this->mpdf->OTLtags['FFMinus'];
1163 preg_match_all('/([a-zA-Z0-9]{4})/',$ffm,$m);
1164 for($i=0;$i<count($m[0]);$i++) {
1165 $t = $m[1][$i];
1166 // Is it a valid tag?
1167 if(isset($Features[$t]) && strpos($omittags,$t)===false && (!$onlytags || strpos($tags,$t)!==false )) {
1168 $usetags = str_replace($t,'',$usetags);
1169 }
1170 }
1171 return $usetags;
1172 }
1173
1174 function _applyGSUBrules($usetags, $scriptTag, $langsys) {
1175 // Features from all Tags are applied together, in Lookup List order.
1176 // For Indic - should be applied one syllable at a time
1177 // - Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1178 // if $this->restrictToSyllable is true
1179
1180 $GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1181 $LookupList = array();
1182 foreach($GSUBFeatures AS $tag=>$arr) {
1183 if (strpos($usetags, $tag)!==false) {
1184 foreach($arr AS $lu) { $LookupList[$lu] = $tag; }
1185 }
1186 }
1187 ksort($LookupList);
1188
1189 foreach($LookupList AS $lu=>$tag) {
1190 $Type = $this->GSUBLookups[$lu]['Type'];
1191 $Flag = $this->GSUBLookups[$lu]['Flag'];
1192 $MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1193 $tagInt = 1;
1194 if (preg_match('/'.$tag.'([0-9]{1,2})/', $usetags, $m)) {
1195 $tagInt = $m[1];
1196 }
1197 $ptr = 0;
1198 // Test each glyph sequentially
1199 while($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1200 $currGlyph = $this->OTLdata[$ptr]['hex'];
1201 $currGID = $this->OTLdata[$ptr]['uni'];
1202 $shift = 1;
1203 foreach($this->GSUBLookups[$lu]['Subtables'] AS $c=>$subtable_offset) {
1204 // NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1205 if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1206 // Get rules from font GSUB subtable
1207 $shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1208
1209 if ($shift) { break; }
1210 }
1211 }
1212 if ($shift == 0) { $shift = 1; }
1213 $ptr += $shift;
1214
1215 }
1216 }
1217 }
1218
1219 function _applyGSUBrulesSingly($usetags, $scriptTag, $langsys) {
1220 // Features are applied one at a time, working through each codepoint
1221
1222 $GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1223
1224 $tags = explode(' ',$usetags);
1225 foreach($tags AS $usetag) {
1226 $LookupList = array();
1227 foreach($GSUBFeatures AS $tag=>$arr) {
1228 if (strpos($usetags, $tag)!==false) {
1229 foreach($arr AS $lu) { $LookupList[$lu] = $tag; }
1230 }
1231 }
1232 ksort($LookupList);
1233
1234 $ptr = 0;
1235 // Test each glyph sequentially
1236 while($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1237 $currGlyph = $this->OTLdata[$ptr]['hex'];
1238 $currGID = $this->OTLdata[$ptr]['uni'];
1239 $shift = 1;
1240
1241 foreach($LookupList AS $lu=>$tag) {
1242 $Type = $this->GSUBLookups[$lu]['Type'];
1243 $Flag = $this->GSUBLookups[$lu]['Flag'];
1244 $MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1245 $tagInt = 1;
1246 if (preg_match('/'.$tag.'([0-9]{1,2})/', $usetags, $m)) {
1247 $tagInt = $m[1];
1248 }
1249
1250 foreach($this->GSUBLookups[$lu]['Subtables'] AS $c=>$subtable_offset) {
1251 // NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1252 if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1253 // Get rules from font GSUB subtable
1254 $shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1255
1256 if ($shift) { break 2; }
1257 }
1258 }
1259 }
1260 if ($shift == 0) { $shift = 1; }
1261 $ptr += $shift;
1262
1263 }
1264 }
1265 }
1266
1267 function _applyGSUBrulesMyanmar($usetags, $scriptTag, $langsys) {
1268 // $usetags = locl ccmp rphf pref blwf pstf';
1269 // applied to all characters
1270
1271 $GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1272
1273 // ALL should be applied one syllable at a time
1274 // Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1275 $tags = explode(' ',$usetags);
1276 foreach($tags AS $usetag) {
1277
1278 $LookupList = array();
1279 foreach($GSUBFeatures AS $tag=>$arr) {
1280 if ($tag==$usetag) {
1281 foreach($arr AS $lu) { $LookupList[$lu] = $tag; }
1282 }
1283 }
1284 ksort($LookupList);
1285
1286 foreach($LookupList AS $lu=>$tag) {
1287
1288 $Type = $this->GSUBLookups[$lu]['Type'];
1289 $Flag = $this->GSUBLookups[$lu]['Flag'];
1290 $MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1291 $tagInt = 1;
1292 if (preg_match('/'.$tag.'([0-9]{1,2})/', $usetags, $m)) {
1293 $tagInt = $m[1];
1294 }
1295
1296 $ptr = 0;
1297 // Test each glyph sequentially
1298 while($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1299 $currGlyph = $this->OTLdata[$ptr]['hex'];
1300 $currGID = $this->OTLdata[$ptr]['uni'];
1301 $shift = 1;
1302 foreach($this->GSUBLookups[$lu]['Subtables'] AS $c=>$subtable_offset) {
1303 // NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1304 if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1305 // Get rules from font GSUB subtable
1306 $shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $usetag, 0, $tagInt);
1307
1308 if ($shift) { break; }
1309 }
1310 }
1311 if ($shift == 0) { $shift = 1; }
1312 $ptr += $shift;
1313
1314 }
1315 }
1316 }
1317 }
1318
1319 function _applyGSUBrulesIndic($usetags, $scriptTag, $langsys, $is_old_spec) {
1320 // $usetags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct'; then later - init
1321 // rphf, pref, blwf, half, abvf, pstf, and init are only applied where ['mask'] indicates: INDIC::FLAG(INDIC::RPHF);
1322 // The rest are applied to all characters
1323
1324 $GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1325
1326 // ALL should be applied one syllable at a time
1327 // Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1328 $tags = explode(' ',$usetags);
1329 foreach($tags AS $usetag) {
1330
1331 $LookupList = array();
1332 foreach($GSUBFeatures AS $tag=>$arr) {
1333 if ($tag==$usetag) {
1334 foreach($arr AS $lu) { $LookupList[$lu] = $tag; }
1335 }
1336 }
1337 ksort($LookupList);
1338
1339 foreach($LookupList AS $lu=>$tag) {
1340
1341 $Type = $this->GSUBLookups[$lu]['Type'];
1342 $Flag = $this->GSUBLookups[$lu]['Flag'];
1343 $MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1344 $tagInt = 1;
1345 if (preg_match('/'.$tag.'([0-9]{1,2})/', $usetags, $m)) {
1346 $tagInt = $m[1];
1347 }
1348
1349 $ptr = 0;
1350 // Test each glyph sequentially
1351 while($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1352 $currGlyph = $this->OTLdata[$ptr]['hex'];
1353 $currGID = $this->OTLdata[$ptr]['uni'];
1354 $shift = 1;
1355 foreach($this->GSUBLookups[$lu]['Subtables'] AS $c=>$subtable_offset) {
1356 // NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1357 if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1358 if (strpos('rphf pref blwf half pstf cfar init' , $usetag)!==false) { // only apply when mask indicates
1359 $mask = 0;
1360 switch ($usetag) {
1361 case 'rphf': $mask = (1<<(INDIC::RPHF)); break;
1362 case 'pref': $mask = (1<<(INDIC::PREF)); break;
1363 case 'blwf': $mask = (1<<(INDIC::BLWF)); break;
1364 case 'half': $mask = (1<<(INDIC::HALF)); break;
1365 case 'pstf': $mask = (1<<(INDIC::PSTF)); break;
1366 case 'cfar': $mask = (1<<(INDIC::CFAR)); break;
1367 case 'init': $mask = (1<<(INDIC::INIT)); break;
1368 }
1369 if (!($this->OTLdata[$ptr]['mask'] & $mask)) { continue; }
1370 }
1371 // Get rules from font GSUB subtable
1372 $shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $usetag, $is_old_spec, $tagInt);
1373
1374 if ($shift) { break; }
1375 }
1376
1377 // Special case for Indic ZZZ99S
1378 // Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1379 // i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1380 // incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1381 // See also ttffontsuni.php
1382 // First check if current glyph is a Halant/Virama
1383 else if (_OTL_OLD_SPEC_COMPAT_1 && $Type==4 && !$is_old_spec && strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D',$currGlyph)!== false) {
1384 // only apply when 'pref blwf pstf' tags, and when mask indicates
1385 if (strpos('pref blwf pstf' , $usetag)!==false) {
1386 $mask = 0;
1387 switch ($usetag) {
1388 case 'pref': $mask = (1<<(INDIC::PREF)); break;
1389 case 'blwf': $mask = (1<<(INDIC::BLWF)); break;
1390 case 'pstf': $mask = (1<<(INDIC::PSTF)); break;
1391 }
1392 if (!($this->OTLdata[$ptr]['mask'] & $mask)) { continue; }
1393
1394 $nextGlyph = $this->OTLdata[$ptr+1]['hex'];
1395 $nextGID = $this->OTLdata[$ptr+1]['uni'];
1396 if (isset($this->GSLuCoverage[$lu][$c][$nextGID])) {
1397
1398 // Get rules from font GSUB subtable
1399 $shift = $this->_applyGSUBsubtableSpecial($lu, $c, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, ($subtable_offset - $this->GSUB_offset), $Type, $this->GSLuCoverage[$lu][$c]);
1400
1401 if ($shift) { break; }
1402 }
1403 }
1404 }
1405
1406
1407 }
1408 if ($shift == 0) { $shift = 1; }
1409 $ptr += $shift;
1410
1411 }
1412 }
1413 }
1414 }
1415
1416
1417 function _applyGSUBsubtableSpecial($lookupID, $subtable, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, $subtable_offset, $Type, $LuCoverage) {
1418
1419 // Special case for Indic
1420 // Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1421 // i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1422 // incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1423 // See also ttffontsuni.php
1424
1425 $this->seek($subtable_offset);
1426 $SubstFormat= $this->read_ushort();
1427
1428 // Subtable contains Consonant - Halant
1429 // Text string contains Halant ($CurrGlyph) - Consonant ($nextGlyph)
1430 // Halant has already been matched, and already checked that $nextGID is in Coverage table
1431
1432 ////////////////////////////////////////////////////////////////////////////////
1433 // Only does: LookupType 4: Ligature Substitution Subtable : n to 1
1434 ////////////////////////////////////////////////////////////////////////////////
1435 $Coverage = $subtable_offset + $this->read_ushort();
1436 $NextGlyphPos = $LuCoverage[$nextGID];
1437 $LigSetCount = $this->read_short();
1438
1439 $this->skip($NextGlyphPos * 2);
1440 $LigSet = $subtable_offset + $this->read_short();
1441
1442 $this->seek($LigSet);
1443 $LigCount = $this->read_short();
1444 // LigatureSet i.e. all starting with the same Glyph $nextGlyph [Consonant]
1445 $LigatureOffset = array();
1446 for ($g=0;$g<$LigCount;$g++) {
1447 $LigatureOffset[$g] = $LigSet + $this->read_ushort();
1448 }
1449 for ($g=0;$g<$LigCount;$g++) {
1450 // Ligature tables
1451 $this->seek($LigatureOffset[$g]);
1452 $LigGlyph = $this->read_ushort();
1453 $substitute = $this->glyphToChar($LigGlyph);
1454 $CompCount = $this->read_ushort();
1455
1456 if ($CompCount != 2) { return 0; } // Only expecting to work with 2:1 (and no ignore characters in between)
1457
1458
1459 $gid = $this->read_ushort();
1460 $checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1461
1462 if ($currGID == $checkGlyph) { $match = true; }
1463 else { $match = false; break; }
1464
1465 $GlyphPos = array();
1466 $GlyphPos[] = $ptr;
1467 $GlyphPos[] = $ptr+1;
1468
1469
1470 if ($match) {
1471 $shift = $this->GSUBsubstitute($ptr, $substitute, 4, $GlyphPos ); // GlyphPos contains positions to set null
1472 if ($shift) return 1;
1473 }
1474
1475 }
1476 return 0;
1477 }
1478
1479 function _applyGSUBsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $level=0, $currentTag, $is_old_spec, $tagInt) {
1480 $ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
1481
1482 // Lets start
1483 $this->seek($subtable_offset);
1484 $SubstFormat= $this->read_ushort();
1485
1486 ////////////////////////////////////////////////////////////////////////////////
1487 // LookupType 1: Single Substitution Subtable : 1 to 1
1488 ////////////////////////////////////////////////////////////////////////////////
1489 if ($Type == 1) {
1490 // Flag = Ignore
1491 if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) { return 0; }
1492 $CoverageOffset = $subtable_offset + $this->read_ushort();
1493 $GlyphPos = $LuCoverage[$currGID];
1494 //===========
1495 // Format 1:
1496 //===========
1497 if ($SubstFormat==1) { // Calculated output glyph indices
1498 $DeltaGlyphID = $this->read_short();
1499 $this->seek($CoverageOffset);
1500 $glyphs = $this->_getCoverageGID();
1501 $GlyphID = $glyphs[$GlyphPos] + $DeltaGlyphID;
1502 }
1503 //===========
1504 // Format 2:
1505 //===========
1506 else if ($SubstFormat==2) { // Specified output glyph indices
1507 $GlyphCount = $this->read_ushort();
1508 $this->skip($GlyphPos * 2 );
1509 $GlyphID = $this->read_ushort();
1510 }
1511
1512 $substitute = $this->glyphToChar($GlyphID);
1513 $shift = $this->GSUBsubstitute($ptr, $substitute, $Type );
1514 if ($this->debugOTL && $shift) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1515 if ($shift) return 1;
1516 return 0;
1517 }
1518
1519 ////////////////////////////////////////////////////////////////////////////////
1520 // LookupType 2: Multiple Substitution Subtable : 1 to n
1521 ////////////////////////////////////////////////////////////////////////////////
1522 else if ($Type == 2) {
1523 // Flag = Ignore
1524 if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) { return 0; }
1525 $Coverage = $subtable_offset + $this->read_ushort();
1526 $GlyphPos = $LuCoverage[$currGID];
1527 $this->skip(2);
1528 $this->skip($GlyphPos * 2);
1529 $Sequences = $subtable_offset + $this->read_short();
1530
1531 $this->seek($Sequences);
1532 $GlyphCount = $this->read_short();
1533 $SubstituteGlyphs = array();
1534 for ($g=0;$g<$GlyphCount;$g++) {
1535 $sgid = $this->read_ushort();
1536 $SubstituteGlyphs[] = $this->glyphToChar($sgid);
1537 }
1538
1539 $shift = $this->GSUBsubstitute($ptr, $SubstituteGlyphs, $Type );
1540 if ($this->debugOTL && $shift) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1541 if ($shift) return $shift;
1542 return 0;
1543 }
1544 ////////////////////////////////////////////////////////////////////////////////
1545 // LookupType 3: Alternate Forms : 1 to 1(n)
1546 ////////////////////////////////////////////////////////////////////////////////
1547 else if ($Type == 3) {
1548 // Flag = Ignore
1549 if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) { return 0; }
1550 $Coverage = $subtable_offset + $this->read_ushort();
1551 $AlternateSetCount = $this->read_short();
1552 ///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1553 // Need to set alternate IF set by CSS3 font-feature for a tag
1554 // i.e. if this is 'salt' alternate may be set to 2
1555 // default value will be $alt=1 ( === index of 0 in list of alternates)
1556 $alt = 1; // $alt=1 points to Alternative[0]
1557 if ($tagInt>1) { $alt = $tagInt; }
1558 ///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1559 if ($alt == 0) { return 0; } // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1560
1561 $GlyphPos = $LuCoverage[$currGID];
1562 $this->skip($GlyphPos * 2);
1563
1564 $AlternateSets = $subtable_offset + $this->read_short();
1565 $this->seek($AlternateSets );
1566
1567 $AlternateGlyphCount = $this->read_short();
1568 if ($alt > $AlternateGlyphCount) { return 0; } // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1569
1570 $this->skip(($alt-1) * 2);
1571 $GlyphID = $this->read_ushort();
1572
1573 $substitute = $this->glyphToChar($GlyphID);
1574 $shift = $this->GSUBsubstitute($ptr, $substitute, $Type );
1575 if ($this->debugOTL && $shift) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1576 if ($shift) return 1;
1577 return 0;
1578 }
1579 ////////////////////////////////////////////////////////////////////////////////
1580 // LookupType 4: Ligature Substitution Subtable : n to 1
1581 ////////////////////////////////////////////////////////////////////////////////
1582 else if ($Type == 4) {
1583 // Flag = Ignore
1584 if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) { return 0; }
1585 $Coverage = $subtable_offset + $this->read_ushort();
1586 $FirstGlyphPos = $LuCoverage[$currGID];
1587
1588 $LigSetCount = $this->read_short();
1589
1590 $this->skip($FirstGlyphPos * 2);
1591 $LigSet = $subtable_offset + $this->read_short();
1592
1593 $this->seek($LigSet);
1594 $LigCount = $this->read_short();
1595 // LigatureSet i.e. all starting with the same first Glyph $currGlyph
1596 $LigatureOffset = array();
1597 for ($g=0;$g<$LigCount;$g++) {
1598 $LigatureOffset[$g] = $LigSet + $this->read_ushort();
1599 }
1600 for ($g=0;$g<$LigCount;$g++) {
1601 // Ligature tables
1602 $this->seek($LigatureOffset[$g]);
1603 $LigGlyph = $this->read_ushort(); // Output Ligature GlyphID
1604 $substitute = $this->glyphToChar($LigGlyph);
1605 $CompCount = $this->read_ushort();
1606
1607 $spos = $ptr;
1608 $match = true;
1609 $GlyphPos = array();
1610 $GlyphPos[] = $spos;
1611 for ($l=1;$l<$CompCount;$l++) {
1612 $gid = $this->read_ushort();
1613 $checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1614
1615 $spos++;
1616 //while $this->OTLdata[$spos]['uni'] is an "ignore" => spos++
1617 while (isset($this->OTLdata[$spos]) && strpos($ignore, $this->OTLdata[$spos]['hex'])!==false) { $spos++; }
1618
1619 if (isset($this->OTLdata[$spos]) && $this->OTLdata[$spos]['uni'] == $checkGlyph) {
1620 $GlyphPos[] = $spos;
1621 }
1622 else { $match = false; break; }
1623
1624 }
1625
1626
1627 if ($match) {
1628 $shift = $this->GSUBsubstitute($ptr, $substitute, $Type, $GlyphPos ); // GlyphPos contains positions to set null
1629 if ($this->debugOTL && $shift) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1630 if ($shift) return ($spos-$ptr+1-($CompCount-1));
1631 }
1632
1633 }
1634 return 0;
1635 }
1636
1637 ////////////////////////////////////////////////////////////////////////////////
1638 // LookupType 5: Contextual Substitution Subtable
1639 ////////////////////////////////////////////////////////////////////////////////
1640 else if ($Type == 5) {
1641 //===========
1642 // Format 1: Simple Context Glyph Substitution
1643 //===========
1644 if ($SubstFormat==1) {
1645 $CoverageTableOffset = $subtable_offset + $this->read_ushort();
1646 $SubRuleSetCount = $this->read_ushort();
1647 $SubRuleSetOffset = array();
1648 for ($b=0;$b<$SubRuleSetCount;$b++) {
1649 $offset = $this->read_ushort();
1650 if ($offset==0x0000) {
1651 $SubRuleSetOffset[] = $offset;
1652 }
1653 else {
1654 $SubRuleSetOffset[] = $subtable_offset + $offset;
1655 }
1656 }
1657
1658 // SubRuleSet tables: All contexts beginning with the same glyph
1659 // Select the SubRuleSet required using the position of the glyph in the coverage table
1660 $GlyphPos = $LuCoverage[$currGID];
1661 if ($SubRuleSetOffset[$GlyphPos]>0) {
1662 $this->seek($SubRuleSetOffset[$GlyphPos]);
1663 $SubRuleCnt = $this->read_ushort();
1664 $SubRule = array();
1665 for($b=0;$b<$SubRuleCnt;$b++) {
1666 $SubRule[$b] = $SubRuleSetOffset[$GlyphPos]+$this->read_ushort();
1667 }
1668 for($b=0;$b<$SubRuleCnt;$b++) { // EACH RULE
1669 $this->seek($SubRule[$b]);
1670 $InputGlyphCount = $this->read_ushort();
1671 $SubstCount = $this->read_ushort();
1672
1673 $Backtrack = array();
1674 $Lookahead = array();
1675 $Input = array();
1676 $Input[0] = $this->OTLdata[$ptr]['uni'];
1677 for ($r=1;$r<$InputGlyphCount;$r++) {
1678 $gid = $this->read_ushort();
1679 $Input[$r] = $this->glyphToChar($gid);
1680 }
1681 $matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
1682 if ($matched) {
1683 if ($this->debugOTL) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1684 for ($p=0;$p<$SubstCount;$p++) { // EACH LOOKUP
1685 $SequenceIndex[$p] = $this->read_ushort();
1686 $LookupListIndex[$p] = $this->read_ushort();
1687 }
1688
1689 for ($p=0;$p<$SubstCount;$p++) {
1690 // Apply $LookupListIndex at $SequenceIndex
1691 if ($SequenceIndex[$p] >= $InputGlyphCount) { continue; }
1692 $lu = $LookupListIndex[$p];
1693 $luType = $this->GSUBLookups[$lu]['Type'];
1694 $luFlag = $this->GSUBLookups[$lu]['Flag'];
1695 $luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1696
1697 $luptr = $matched[$SequenceIndex[$p]];
1698 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1699 $lucurrGID = $this->OTLdata[$luptr]['uni'];
1700
1701 foreach($this->GSUBLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
1702 $shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset) , $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
1703 if ($shift) { break; }
1704 }
1705 }
1706
1707 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
1708 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
1709
1710 }
1711 }
1712
1713 }
1714 return 0;
1715 }
1716
1717 //===========
1718 // Format 2:
1719 //===========
1720 // Format 2: Class-based Context Glyph Substitution
1721 else if ($SubstFormat==2) {
1722
1723 $CoverageTableOffset = $subtable_offset + $this->read_ushort();
1724 $InputClassDefOffset = $subtable_offset + $this->read_ushort();
1725 $SubClassSetCnt = $this->read_ushort();
1726 $SubClassSetOffset = array();
1727 for ($b=0;$b<$SubClassSetCnt;$b++) {
1728 $offset = $this->read_ushort();
1729 if ($offset==0x0000) {
1730 $SubClassSetOffset[] = $offset;
1731 }
1732 else {
1733 $SubClassSetOffset[] = $subtable_offset + $offset;
1734 }
1735 }
1736
1737 $InputClasses = $this->_getClasses($InputClassDefOffset);
1738
1739 for ($s=0;$s<$SubClassSetCnt;$s++) { // $SubClassSet is ordered by input class-may be NULL
1740 // Select $SubClassSet if currGlyph is in First Input Class
1741 if ($SubClassSetOffset[$s]>0 && isset($InputClasses[$s][$currGID])) {
1742 $this->seek($SubClassSetOffset[$s]);
1743 $SubClassRuleCnt = $this->read_ushort();
1744 $SubClassRule = array();
1745 for($b=0;$b<$SubClassRuleCnt;$b++) {
1746 $SubClassRule[$b] = $SubClassSetOffset[$s]+$this->read_ushort();
1747 }
1748
1749 for($b=0;$b<$SubClassRuleCnt;$b++) { // EACH RULE
1750 $this->seek($SubClassRule[$b]);
1751 $InputGlyphCount = $this->read_ushort();
1752 $SubstCount = $this->read_ushort();
1753 $Input = array();
1754 for ($r=1;$r<$InputGlyphCount;$r++) {
1755 $Input[$r] = $this->read_ushort();
1756 }
1757
1758 $inputClass = $s;
1759
1760 $inputGlyphs = array();
1761 $inputGlyphs[0] = $InputClasses[$inputClass];
1762
1763 if ($InputGlyphCount>1) {
1764 // NB starts at 1
1765 for ($gcl=1;$gcl<$InputGlyphCount;$gcl++) {
1766 $classindex = $Input[$gcl];
1767 if (isset($InputClasses[$classindex])) { $inputGlyphs[$gcl] = $InputClasses[$classindex]; }
1768 else { $inputGlyphs[$gcl] = ''; }
1769 }
1770 }
1771
1772 // Class 0 contains all the glyphs NOT in the other classes
1773 $class0excl = array();
1774 for ($gc=1;$gc<=count($InputClasses);$gc++) {
1775 if (is_array($InputClasses[$gc])) $class0excl = $class0excl + $InputClasses[$gc];
1776 }
1777
1778 $backtrackGlyphs = array();
1779 $lookaheadGlyphs = array();
1780
1781 $matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl);
1782 if ($matched) {
1783 if ($this->debugOTL) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1784 for ($p=0;$p<$SubstCount;$p++) { // EACH LOOKUP
1785 $SequenceIndex[$p] = $this->read_ushort();
1786 $LookupListIndex[$p] = $this->read_ushort();
1787 }
1788
1789 for ($p=0;$p<$SubstCount;$p++) {
1790 // Apply $LookupListIndex at $SequenceIndex
1791 if ($SequenceIndex[$p] >= $InputGlyphCount) { continue; }
1792 $lu = $LookupListIndex[$p];
1793 $luType = $this->GSUBLookups[$lu]['Type'];
1794 $luFlag = $this->GSUBLookups[$lu]['Flag'];
1795 $luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1796
1797 $luptr = $matched[$SequenceIndex[$p]];
1798 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1799 $lucurrGID = $this->OTLdata[$luptr]['uni'];
1800
1801 foreach($this->GSUBLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
1802 $shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset) , $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
1803 if ($shift) { break; }
1804 }
1805 }
1806
1807 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
1808 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
1809
1810 }
1811
1812 }
1813
1814 }
1815 }
1816
1817 return 0;
1818 }
1819
1820 //===========
1821 // Format 3:
1822 //===========
1823 // Format 3: Coverage-based Context Glyph Substitution
1824 else if ($SubstFormat==3) {
1825 die("GSUB Lookup Type ".$Type." Format ".$SubstFormat." not TESTED YET.");
1826 return 0;
1827 }
1828
1829 }
1830
1831 ////////////////////////////////////////////////////////////////////////////////
1832 // LookupType 6: Chaining Contextual Substitution Subtable
1833 ////////////////////////////////////////////////////////////////////////////////
1834 else if ($Type == 6) {
1835
1836 //===========
1837 // Format 1:
1838 //===========
1839 // Format 1: Simple Chaining Context Glyph Substitution
1840 if ($SubstFormat==1) {
1841 $Coverage = $subtable_offset + $this->read_ushort();
1842 $GlyphPos = $LuCoverage[$currGID];
1843 $ChainSubRuleSetCount = $this->read_ushort();
1844 // All of the ChainSubRule tables defining contexts that begin with the same first glyph are grouped together and defined in a ChainSubRuleSet table
1845 $this->skip($GlyphPos * 2);
1846 $ChainSubRuleSet= $subtable_offset + $this->read_ushort();
1847 $this->seek($ChainSubRuleSet);
1848 $ChainSubRuleCount = $this->read_ushort();
1849
1850 for($s=0;$s<$ChainSubRuleCount;$s++) {
1851 $ChainSubRule[$s] = $ChainSubRuleSet + $this->read_ushort();
1852 }
1853
1854 for($s=0;$s<$ChainSubRuleCount;$s++) {
1855 $this->seek($ChainSubRule[$s]);
1856
1857 $BacktrackGlyphCount = $this->read_ushort();
1858 $Backtrack = array();
1859 for ($b=0;$b<$BacktrackGlyphCount;$b++) {
1860 $gid = $this->read_ushort();
1861 $Backtrack[] = $this->glyphToChar($gid);
1862 }
1863 $Input = array();
1864 $Input[0] = $this->OTLdata[$ptr]['uni'];
1865 $InputGlyphCount = $this->read_ushort();
1866 for ($b=1;$b<$InputGlyphCount;$b++) {
1867 $gid = $this->read_ushort();
1868 $Input[$b] = $this->glyphToChar($gid);
1869 }
1870 $LookaheadGlyphCount = $this->read_ushort();
1871 $Lookahead = array();
1872 for ($b=0;$b<$LookaheadGlyphCount;$b++) {
1873 $gid = $this->read_ushort();
1874 $Lookahead[] = $this->glyphToChar($gid);
1875 }
1876
1877 $matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
1878 if ($matched) {
1879 if ($this->debugOTL) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
1880 $SubstCount = $this->read_ushort();
1881 for ($p=0;$p<$SubstCount;$p++) {
1882 // SubstLookupRecord
1883 $SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
1884 $SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
1885 }
1886 for ($p=0;$p<$SubstCount;$p++) {
1887 // Apply $SubstLookupRecord[$p]['LookupListIndex'] at $SubstLookupRecord[$p]['SequenceIndex']
1888 if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) { continue; }
1889 $lu = $SubstLookupRecord[$p]['LookupListIndex'];
1890 $luType = $this->GSUBLookups[$lu]['Type'];
1891 $luFlag = $this->GSUBLookups[$lu]['Flag'];
1892 $luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1893
1894 $luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
1895 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1896 $lucurrGID = $this->OTLdata[$luptr]['uni'];
1897
1898 foreach($this->GSUBLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
1899 $shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
1900 if ($shift) { break; }
1901 }
1902 }
1903 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
1904 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
1905 }
1906
1907
1908
1909
1910 }
1911 return 0;
1912 }
1913
1914 //===========
1915 // Format 2:
1916 //===========
1917 // Format 2: Class-based Chaining Context Glyph Substitution p257
1918 else if ($SubstFormat==2) {
1919
1920 // NB Format 2 specifies fixed class assignments (identical for each position in the backtrack, input, or lookahead sequence) and exclusive classes (a glyph cannot be in more than one class at a time)
1921
1922 $CoverageTableOffset = $subtable_offset + $this->read_ushort();
1923 $BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
1924 $InputClassDefOffset = $subtable_offset + $this->read_ushort();
1925 $LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
1926 $ChainSubClassSetCnt = $this->read_ushort();
1927 $ChainSubClassSetOffset = array();
1928 for ($b=0;$b<$ChainSubClassSetCnt;$b++) {
1929 $offset = $this->read_ushort();
1930 if ($offset==0x0000) {
1931 $ChainSubClassSetOffset[] = $offset;
1932 }
1933 else {
1934 $ChainSubClassSetOffset[] = $subtable_offset + $offset;
1935 }
1936 }
1937
1938 $BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
1939 $InputClasses = $this->_getClasses($InputClassDefOffset);
1940 $LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
1941
1942 for ($s=0;$s<$ChainSubClassSetCnt;$s++) { // $ChainSubClassSet is ordered by input class-may be NULL
1943 // Select $ChainSubClassSet if currGlyph is in First Input Class
1944 if ($ChainSubClassSetOffset[$s]>0 && isset($InputClasses[$s][$currGID])) {
1945 $this->seek($ChainSubClassSetOffset[$s]);
1946 $ChainSubClassRuleCnt = $this->read_ushort();
1947 $ChainSubClassRule = array();
1948 for($b=0;$b<$ChainSubClassRuleCnt;$b++) {
1949 $ChainSubClassRule[$b] = $ChainSubClassSetOffset[$s]+$this->read_ushort();
1950 }
1951
1952 for($b=0;$b<$ChainSubClassRuleCnt;$b++) { // EACH RULE
1953 $this->seek($ChainSubClassRule[$b]);
1954 $BacktrackGlyphCount = $this->read_ushort();
1955 for ($r=0;$r<$BacktrackGlyphCount;$r++) {
1956 $Backtrack[$r] = $this->read_ushort();
1957 }
1958 $InputGlyphCount = $this->read_ushort();
1959 for ($r=1;$r<$InputGlyphCount;$r++) {
1960 $Input[$r] = $this->read_ushort();
1961 }
1962 $LookaheadGlyphCount = $this->read_ushort();
1963 for ($r=0;$r<$LookaheadGlyphCount;$r++) {
1964 $Lookahead[$r] = $this->read_ushort();
1965 }
1966
1967
1968 // These contain classes of glyphs as arrays
1969 // $InputClasses[(class)] e.g. 0x02E6,0x02E7,0x02E8
1970 // $LookaheadClasses[(class)]
1971 // $BacktrackClasses[(class)]
1972
1973 // These contain arrays of classIndexes
1974 // [Backtrack] [Lookahead] and [Input] (Input is from the second position only)
1975
1976
1977 $inputClass = $s; //???
1978
1979 $inputGlyphs = array();
1980 $inputGlyphs[0] = $InputClasses[$inputClass];
1981
1982 if ($InputGlyphCount>1) {
1983 // NB starts at 1
1984 for ($gcl=1;$gcl<$InputGlyphCount;$gcl++) {
1985 $classindex = $Input[$gcl];
1986 if (isset($InputClasses[$classindex])) { $inputGlyphs[$gcl] = $InputClasses[$classindex]; }
1987 else { $inputGlyphs[$gcl] = ''; }
1988 }
1989 }
1990
1991 // Class 0 contains all the glyphs NOT in the other classes
1992 $class0excl = array();
1993 for ($gc=1;$gc<=count($InputClasses);$gc++) {
1994 if (isset($InputClasses[$gc])) $class0excl = $class0excl + $InputClasses[$gc];
1995 }
1996
1997 if ($BacktrackGlyphCount) {
1998 for ($gcl=0;$gcl<$BacktrackGlyphCount;$gcl++) {
1999 $classindex = $Backtrack[$gcl];
2000 if (isset($BacktrackClasses[$classindex])) { $backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex]; }
2001 else { $backtrackGlyphs[$gcl] = ''; }
2002 }
2003 }
2004 else { $backtrackGlyphs = array(); }
2005
2006 // Class 0 contains all the glyphs NOT in the other classes
2007 $bclass0excl = array();
2008 for ($gc=1;$gc<=count($BacktrackClasses);$gc++) {
2009 if (isset($BacktrackClasses[$gc])) $bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
2010 }
2011
2012
2013 if ($LookaheadGlyphCount) {
2014 for ($gcl=0;$gcl<$LookaheadGlyphCount;$gcl++) {
2015 $classindex = $Lookahead[$gcl];
2016 if (isset($LookaheadClasses[$classindex])) { $lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex]; }
2017 else { $lookaheadGlyphs[$gcl] = ''; }
2018 }
2019 }
2020 else { $lookaheadGlyphs = array(); }
2021
2022 // Class 0 contains all the glyphs NOT in the other classes
2023 $lclass0excl = array();
2024 for ($gc=1;$gc<=count($LookaheadClasses);$gc++) {
2025 if (isset($LookaheadClasses[$gc])) $lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
2026 }
2027
2028
2029 $matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl );
2030 if ($matched) {
2031 if ($this->debugOTL) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
2032 $SubstCount = $this->read_ushort();
2033 for ($p=0;$p<$SubstCount;$p++) { // EACH LOOKUP
2034 $SequenceIndex[$p] = $this->read_ushort();
2035 $LookupListIndex[$p] = $this->read_ushort();
2036 }
2037
2038 for ($p=0;$p<$SubstCount;$p++) {
2039 // Apply $LookupListIndex at $SequenceIndex
2040 if ($SequenceIndex[$p] >= $InputGlyphCount) { continue; }
2041 $lu = $LookupListIndex[$p];
2042 $luType = $this->GSUBLookups[$lu]['Type'];
2043 $luFlag = $this->GSUBLookups[$lu]['Flag'];
2044 $luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2045
2046 $luptr = $matched[$SequenceIndex[$p]];
2047 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2048 $lucurrGID = $this->OTLdata[$luptr]['uni'];
2049
2050 foreach($this->GSUBLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
2051 $shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset) , $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
2052 if ($shift) { break; }
2053 }
2054 }
2055
2056 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
2057 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
2058
2059 }
2060
2061 }
2062
2063 }
2064 }
2065
2066 return 0;
2067 }
2068
2069 //===========
2070 // Format 3:
2071 //===========
2072 // Format 3: Coverage-based Chaining Context Glyph Substitution p259
2073 else if ($SubstFormat==3) {
2074
2075 $BacktrackGlyphCount = $this->read_ushort();
2076 for ($b=0;$b<$BacktrackGlyphCount;$b++) {
2077 $CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2078 }
2079 $InputGlyphCount = $this->read_ushort();
2080 for ($b=0;$b<$InputGlyphCount;$b++) {
2081 $CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2082 }
2083 $LookaheadGlyphCount = $this->read_ushort();
2084 for ($b=0;$b<$LookaheadGlyphCount;$b++) {
2085 $CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2086 }
2087 $SubstCount = $this->read_ushort();
2088 $save_pos = $this->_pos; // Save the point just after PosCount
2089
2090 $CoverageBacktrackGlyphs = array();
2091 for ($b=0;$b<$BacktrackGlyphCount;$b++) {
2092 $this->seek($CoverageBacktrackOffset[$b]);
2093 $glyphs = $this->_getCoverage();
2094 $CoverageBacktrackGlyphs[$b] = implode("|",$glyphs);
2095 }
2096 $CoverageInputGlyphs = array();
2097 for ($b=0;$b<$InputGlyphCount;$b++) {
2098 $this->seek($CoverageInputOffset[$b]);
2099 $glyphs = $this->_getCoverage();
2100 $CoverageInputGlyphs[$b] = implode("|",$glyphs);
2101 }
2102 $CoverageLookaheadGlyphs = array();
2103 for ($b=0;$b<$LookaheadGlyphCount;$b++) {
2104 $this->seek($CoverageLookaheadOffset[$b]);
2105 $glyphs = $this->_getCoverage();
2106 $CoverageLookaheadGlyphs[$b] = implode("|",$glyphs);
2107 }
2108
2109 $matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs , $ignore, $ptr);
2110 if ($matched) {
2111 if ($this->debugOTL) { $this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level); }
2112
2113 $this->seek($save_pos); // Return to just after PosCount
2114 for ($p=0;$p<$SubstCount;$p++) {
2115 // SubstLookupRecord
2116 $SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
2117 $SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
2118 }
2119 for ($p=0;$p<$SubstCount;$p++) {
2120 // Apply $SubstLookupRecord[$p]['LookupListIndex'] at $SubstLookupRecord[$p]['SequenceIndex']
2121 if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) { continue; }
2122 $lu = $SubstLookupRecord[$p]['LookupListIndex'];
2123 $luType = $this->GSUBLookups[$lu]['Type'];
2124 $luFlag = $this->GSUBLookups[$lu]['Flag'];
2125 $luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2126
2127 $luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
2128 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2129 $lucurrGID = $this->OTLdata[$luptr]['uni'];
2130
2131 foreach($this->GSUBLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
2132 $shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
2133 if ($shift) { break; }
2134 }
2135 }
2136 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return (isset($shift) ? $shift : 0) ; } /* OTL_FIX_3 */
2137 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
2138 }
2139
2140 return 0;
2141
2142 }
2143 }
2144
2145 else { die("GSUB Lookup Type ".$Type." not supported."); }
2146
2147 }
2148
2149 function _updateLigatureMarks($pos, $n) {
2150 if ($n > 0) {
2151 // Update position of Ligatures and associated Marks
2152 // Foreach lig/assocMarks
2153 // Any position lpos or mpos > $pos + count($substitute)
2154 // $this->assocMarks = array(); // assocMarks[$pos mpos] => array(compID, ligPos)
2155 // $this->assocLigs = array(); // Ligatures[$pos lpos] => nc
2156 for ($p=count($this->OTLdata)-1;$p>=($pos+$n);$p--) {
2157 if (isset($this->assocLigs[$p])) {
2158 $tmp = $this->assocLigs[$p];
2159 unset($this->assocLigs[$p]);
2160 $this->assocLigs[($p + $n)] = $tmp;
2161 }
2162 }
2163 for ($p=count($this->OTLdata)-1;$p>=0;$p--) {
2164 if (isset($this->assocMarks[$p])) {
2165 if ($this->assocMarks[$p]['ligPos'] >=($pos+$n)) { $this->assocMarks[$p]['ligPos'] += $n; }
2166 if ($p>=($pos+$n)) {
2167 $tmp = $this->assocMarks[$p];
2168 unset($this->assocMarks[$p]);
2169 $this->assocMarks[($p + $n)] = $tmp;
2170 }
2171 }
2172 }
2173 }
2174
2175 else if ($n<1) { // glyphs removed
2176 $nrem = -$n;
2177 // Update position of pre-existing Ligatures and associated Marks
2178 for ($p=($pos+1);$p<count($this->OTLdata);$p++) {
2179 if (isset($this->assocLigs[$p])) {
2180 $tmp = $this->assocLigs[$p];
2181 unset($this->assocLigs[$p]);
2182 $this->assocLigs[($p - $nrem)] = $tmp;
2183 }
2184 }
2185 for ($p=0;$p<count($this->OTLdata);$p++) {
2186 if (isset($this->assocMarks[$p])) {
2187 if ($this->assocMarks[$p]['ligPos'] >=($pos)) { $this->assocMarks[$p]['ligPos'] -= $nrem; }
2188 if ($p>$pos) {
2189 $tmp = $this->assocMarks[$p];
2190 unset($this->assocMarks[$p]);
2191 $this->assocMarks[($p - $nrem)] = $tmp;
2192 }
2193 }
2194 }
2195 }
2196 }
2197
2198 function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos=NULL ) {
2199
2200 // LookupType 1: Simple Substitution Subtable : 1 to 1
2201 // LookupType 3: Alternate Forms : 1 to 1(n)
2202 if ($Type == 1 || $Type == 3) {
2203 $this->OTLdata[$pos]['uni'] = $substitute;
2204 $this->OTLdata[$pos]['hex'] = $this->unicode_hex($substitute);
2205 return 1;
2206 }
2207 // LookupType 2: Multiple Substitution Subtable : 1 to n
2208 else if ($Type == 2) {
2209 for($i=0;$i<count($substitute);$i++) {
2210 $uni = $substitute[$i];
2211 $newOTLdata[$i] = array();
2212 $newOTLdata[$i]['uni'] = $uni;
2213 $newOTLdata[$i]['hex'] = $this->unicode_hex($uni);
2214
2215
2216 // Get types of new inserted chars - or replicate type of char being replaced
2217 // $bt = UCDN::get_bidi_class($uni);
2218 // if (!$bt) {
2219 $bt = $this->OTLdata[$pos]['bidi_type'];
2220 // }
2221
2222 if (strpos($this->GlyphClassMarks, $newOTLdata[$i]['hex'] )!==false) { $gp = 'M'; }
2223 else if ($uni == 32) { $gp = 'S'; }
2224 else { $gp = 'C'; }
2225
2226 // Need to update matra_type ??? of new glyphs inserted ???????????????????????????????????????
2227
2228 $newOTLdata[$i]['bidi_type'] = $bt;
2229 $newOTLdata[$i]['group'] = $gp;
2230
2231 // Need to update details of new glyphs inserted
2232 $newOTLdata[$i]['general_category'] = $this->OTLdata[$pos]['general_category'];
2233
2234 if ($this->shaper=='I' || $this->shaper=='K' || $this->shaper=='S') {
2235 $newOTLdata[$i]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2236 $newOTLdata[$i]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2237 }
2238 else if ($this->shaper=='M') {
2239 $newOTLdata[$i]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2240 $newOTLdata[$i]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2241 }
2242 if (isset($this->OTLdata[$pos]['mask'])) { $newOTLdata[$i]['mask'] = $this->OTLdata[$pos]['mask']; }
2243 if (isset($this->OTLdata[$pos]['syllable'])) { $newOTLdata[$i]['syllable'] = $this->OTLdata[$pos]['syllable']; }
2244
2245 }
2246 if ($this->shaper=='K' || $this->shaper=='T' || $this->shaper=='L') {
2247 if ($this->OTLdata[$pos]['wordend']) { $newOTLdata[count($substitute)-1]['wordend'] = true; }
2248 }
2249
2250 array_splice($this->OTLdata, $pos, 1, $newOTLdata); // Replace 1 with n
2251 // Update position of Ligatures and associated Marks
2252 // count($substitute)-1 is the number of glyphs added
2253 $nadd = count($substitute)-1;
2254 $this->_updateLigatureMarks($pos, $nadd);
2255 return count($substitute);
2256 }
2257 // LookupType 4: Ligature Substitution Subtable : n to 1
2258 else if ($Type == 4) {
2259 // Create Ligatures and associated Marks
2260 $firstGlyph = $this->OTLdata[$pos]['hex'];
2261
2262 // If all components of the ligature are marks (and in the same syllable), we call this a mark ligature.
2263 $contains_marks = false;
2264 $contains_nonmarks = false;
2265 if (isset($this->OTLdata[$pos]['syllable'])) { $current_syllable = $this->OTLdata[$pos]['syllable']; }
2266 else { $current_syllable = 0; }
2267 for($i=0;$i<count($GlyphPos);$i++) {
2268 // If subsequent components are not Marks as well - don't ligate
2269 $unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2270 if ($this->restrictToSyllable && isset($this->OTLdata[$GlyphPos[$i]]['syllable']) && $this->OTLdata[$GlyphPos[$i]]['syllable'] != $current_syllable) {
2271 return 0;
2272 }
2273 if (strpos($this->GlyphClassMarks, $unistr )!==false) { $contains_marks = true; }
2274 else { $contains_nonmarks = true; }
2275 }
2276 if ($contains_marks && !$contains_nonmarks) {
2277 // Mark Ligature (all components are Marks)
2278 $firstMarkAssoc = '';
2279 if (isset($this->assocMarks[$pos])) {
2280 $firstMarkAssoc = $this->assocMarks[$pos];
2281 }
2282 // If all components of the ligature are marks, we call this a mark ligature.
2283 for($i=1;$i<count($GlyphPos);$i++) {
2284
2285 // If subsequent components are not Marks as well - don't ligate
2286 // $unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2287 // if (strpos($this->GlyphClassMarks, $unistr )===false) { return; }
2288
2289 $nextMarkAssoc = '';
2290 if (isset($this->assocMarks[$GlyphPos[$i]])) {
2291 $nextMarkAssoc = $this->assocMarks[$GlyphPos[$i]];
2292 }
2293 // If first component was attached to a previous ligature component,
2294 // all subsequent components should be attached to the same ligature
2295 // component, otherwise we shouldn't ligate them.
2296 // If first component was NOT attached to a previous ligature component,
2297 // all subsequent components should also NOT be attached to any ligature component,
2298 if ($firstMarkAssoc != $nextMarkAssoc ) {
2299 // unless they are attached to the first component itself!
2300 // if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos']!= $pos) { return; }
2301
2302 // Update/Edit - In test with myanmartext font
2303 // &#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;
2304 // => Lookup 17 E003 E066B E05A 102D
2305 // E003 and 102D should form a mark ligature, but 102D is already associated with (non-mark) ligature E05A
2306 // So instead of disallowing the mark ligature to form, just dissociate...
2307 if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos']!= $pos) { unset($this->assocMarks[$GlyphPos[$i]]); }
2308 }
2309 }
2310
2311 /*
2312 * - If it *is* a mark ligature, we don't allocate a new ligature id, and leave
2313 * the ligature to keep its old ligature id. This will allow it to attach to
2314 * a base ligature in GPOS. Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH,
2315 * and LAM,LAM,HEH form a ligature, they will leave SHADDA and FATHA wit a
2316 * ligature id and component value of 2. Then if SHADDA,FATHA form a ligature
2317 * later, we don't want them to lose their ligature id/component, otherwise
2318 * GPOS will fail to correctly position the mark ligature on top of the
2319 * LAM,LAM,HEH ligature.
2320 */
2321 // So if is_array($firstMarkAssoc) - the new (Mark) ligature should keep this association
2322
2323 $lastPos = $GlyphPos[(count($GlyphPos)-1)];
2324 }
2325 else {
2326 /*
2327 * - Ligatures cannot be formed across glyphs attached to different components
2328 * of previous ligatures. Eg. the sequence is LAM,SHADDA,LAM,FATHA,HEH, and
2329 * LAM,LAM,HEH form a ligature, leaving SHADDA,FATHA next to eachother.
2330 * However, it would be wrong to ligate that SHADDA,FATHA sequence.
2331 * There is an exception to this: If a ligature tries ligating with marks that
2332 * belong to it itself, go ahead, assuming that the font designer knows what
2333 * they are doing (otherwise it can break Indic stuff when a matra wants to
2334 * ligate with a conjunct...)
2335 */
2336
2337 /*
2338 * - If a ligature is formed of components that some of which are also ligatures
2339 * themselves, and those ligature components had marks attached to *their*
2340 * components, we have to attach the marks to the new ligature component
2341 * positions! Now *that*'s tricky! And these marks may be following the
2342 * last component of the whole sequence, so we should loop forward looking
2343 * for them and update them.
2344 *
2345 * Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a
2346 * 'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature
2347 * id and component == 1. Now, during 'liga', the LAM and the LAM-HEH ligature
2348 * form a LAM-LAM-HEH ligature. We need to reassign the SHADDA and FATHA to
2349 * the new ligature with a component value of 2.
2350 *
2351 * This in fact happened to a font... See:
2352 * https://bugzilla.gnome.org/show_bug.cgi?id=437633
2353 */
2354
2355 $currComp = 0;
2356 for($i=0;$i<count($GlyphPos);$i++) {
2357 if ($i>0 && isset($this->assocLigs[$GlyphPos[$i]])) { // One of the other components is already a ligature
2358 $nc = $this->assocLigs[$GlyphPos[$i]];
2359 }
2360 else { $nc = 1; }
2361 // While next char to right is a mark (but not the next matched glyph)
2362 // ?? + also include a Mark Ligature here
2363 $ic = 1;
2364 while((($i==count($GlyphPos)-1) || (isset($GlyphPos[$i+1]) && ($GlyphPos[$i]+$ic) < $GlyphPos[$i+1])) && isset($this->OTLdata[($GlyphPos[$i]+$ic)]) && strpos($this->GlyphClassMarks, $this->OTLdata[($GlyphPos[$i]+$ic)]['hex'])!== false) {
2365 $newComp = $currComp;
2366 if (isset($this->assocMarks[$GlyphPos[$i]+$ic])) { // One of the inbetween Marks is already associated with a Lig
2367 // OK as long as it is associated with the current Lig
2368 // if ($this->assocMarks[($GlyphPos[$i]+$ic)]['ligPos'] != ($GlyphPos[$i]+$ic)) { die("Problem #1"); }
2369 $newComp += $this->assocMarks[($GlyphPos[$i]+$ic)]['compID'];
2370 }
2371 $this->assocMarks[($GlyphPos[$i]+$ic)] = array('compID'=>$newComp, 'ligPos'=>$pos);
2372 $ic++;
2373 }
2374 $currComp += $nc;
2375 }
2376 $lastPos = $GlyphPos[(count($GlyphPos)-1)]+$ic-1;
2377 $this->assocLigs[$pos] = $currComp ; // Number of components in new Ligature
2378 }
2379
2380 // Now remove the unwanted glyphs and associated metadata
2381 $newOTLdata[0] = array();
2382
2383 // Get types of new inserted chars - or replicate type of char being replaced
2384 // $bt = UCDN::get_bidi_class($substitute);
2385 // if (!$bt) {
2386 $bt = $this->OTLdata[$pos]['bidi_type'];
2387 // }
2388
2389 if (strpos($this->GlyphClassMarks, $this->unicode_hex($substitute))!==false) { $gp = 'M'; }
2390 else if ($substitute == 32) { $gp = 'S'; }
2391 else { $gp = 'C'; }
2392
2393 // Need to update details of new glyphs inserted
2394 $newOTLdata[0]['general_category'] = $this->OTLdata[$pos]['general_category'];
2395
2396 $newOTLdata[0]['bidi_type'] = $bt;
2397 $newOTLdata[0]['group'] = $gp;
2398
2399 // KASHIDA: If forming a ligature when the last component was identified as a kashida point (final form)
2400 // If previous/first component of ligature is a medial form, then keep this as a kashida point
2401 // TEST (Arabic Typesetting) &#x64a;&#x64e;&#x646;&#x62a;&#x64f;&#x645;
2402 $ka = 0;
2403 if (isset($this->OTLdata[$GlyphPos[(count($GlyphPos)-1)]]['GPOSinfo']['kashida'])) {
2404 $ka = $this->OTLdata[$GlyphPos[(count($GlyphPos)-1)]]['GPOSinfo']['kashida'];
2405 }
2406 if ($ka==1 && isset($this->OTLdata[$pos]['form']) && $this->OTLdata[$pos]['form']==3) { $newOTLdata[0]['GPOSinfo']['kashida'] = $ka; }
2407
2408 $newOTLdata[0]['uni'] = $substitute;
2409 $newOTLdata[0]['hex'] = $this->unicode_hex($substitute);
2410
2411 if ($this->shaper=='I' || $this->shaper=='K' || $this->shaper=='S') {
2412 $newOTLdata[0]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2413 $newOTLdata[0]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2414 }
2415 else if ($this->shaper=='M') {
2416 $newOTLdata[0]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2417 $newOTLdata[0]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2418 }
2419 if (isset($this->OTLdata[$pos]['mask'])) {$newOTLdata[0]['mask'] = $this->OTLdata[$pos]['mask']; }
2420 if (isset($this->OTLdata[$pos]['syllable'])) {$newOTLdata[0]['syllable'] = $this->OTLdata[$pos]['syllable']; }
2421
2422 $newOTLdata[0]['is_ligature'] = true;
2423
2424
2425 array_splice($this->OTLdata, $pos, 1, $newOTLdata);
2426
2427 // GlyphPos contains array of arr_pos to set null - not necessarily contiguous
2428
2429 // +- Remove any assocMarks or assocLigs from the main components (the ones that are deleted)
2430 for($i=count($GlyphPos)-1;$i>0;$i--) {
2431 $gpos = $GlyphPos[$i];
2432 array_splice($this->OTLdata, $gpos, 1);
2433 unset($this->assocLigs[$gpos]);
2434 unset($this->assocMarks[$gpos]);
2435 }
2436 // $this->assocLigs = array(); // Ligatures[$posarr lpos] => nc
2437 // $this->assocMarks = array(); // assocMarks[$posarr mpos] => array(compID, ligPos)
2438
2439 // Update position of pre-existing Ligatures and associated Marks
2440 // Start after first GlyphPos
2441 // count($GlyphPos)-1 is the number of glyphs removed from string
2442 for($p=($GlyphPos[0]+1);$p<(count($this->OTLdata)+count($GlyphPos)-1) ;$p++ ) {
2443 $nrem = 0; // Number of Glyphs removed at this point in the string
2444 for($i=0;$i<count($GlyphPos);$i++) {
2445 if ($i>0 && $p > $GlyphPos[$i]) { $nrem++; }
2446 }
2447 if (isset($this->assocLigs[$p])) {
2448 $tmp = $this->assocLigs[$p];
2449 unset($this->assocLigs[$p]);
2450 $this->assocLigs[($p - $nrem)] = $tmp;
2451 }
2452 if (isset($this->assocMarks[$p])) {
2453 $tmp = $this->assocMarks[$p];
2454 unset($this->assocMarks[$p]);
2455 if ($tmp['ligPos'] > $GlyphPos[0]) { $tmp['ligPos'] -= $nrem; }
2456 $this->assocMarks[($p - $nrem)] = $tmp;
2457 }
2458 }
2459 return 1;
2460 }
2461 else { return 0; }
2462 }
2463
2464
2465
2466 ////////////////////////////////////////////////////////////////
2467 ////////////////////////////////////////////////////////////////
2468 ////////// ARABIC /////////////////////////////////
2469 ////////////////////////////////////////////////////////////////
2470 ////////////////////////////////////////////////////////////////
2471
2472 function arabic_initialise() {
2473 // cf. http://unicode.org/Public/UNIDATA/ArabicShaping.txt
2474 // http://unicode.org/Public/UNIDATA/extracted/DerivedJoiningType.txt
2475 // JOIN TO FOLLOWING LETTER IN LOGICAL ORDER (i.e. AS INITIAL/MEDIAL FORM) = Unicode Left-Joining (+ Dual-Joining + Join_Causing 00640)
2476 $this->arabLeftJoining = array(
2477 0x0620=>1, 0x0626=>1, 0x0628=>1, 0x062A=>1, 0x062B=>1, 0x062C=>1, 0x062D=>1, 0x062E=>1,
2478 0x0633=>1, 0x0634=>1, 0x0635=>1, 0x0636=>1, 0x0637=>1, 0x0638=>1, 0x0639=>1, 0x063A=>1,
2479 0x063B=>1, 0x063C=>1, 0x063D=>1, 0x063E=>1, 0x063F=>1, 0x0640=>1, 0x0641=>1, 0x0642=>1,
2480 0x0643=>1, 0x0644=>1, 0x0645=>1, 0x0646=>1, 0x0647=>1, 0x0649=>1, 0x064A=>1, 0x066E=>1,
2481 0x066F=>1, 0x0678=>1, 0x0679=>1, 0x067A=>1, 0x067B=>1, 0x067C=>1, 0x067D=>1, 0x067E=>1,
2482 0x067F=>1, 0x0680=>1, 0x0681=>1, 0x0682=>1, 0x0683=>1, 0x0684=>1, 0x0685=>1, 0x0686=>1,
2483 0x0687=>1, 0x069A=>1, 0x069B=>1, 0x069C=>1, 0x069D=>1, 0x069E=>1, 0x069F=>1, 0x06A0=>1,
2484 0x06A1=>1, 0x06A2=>1, 0x06A3=>1, 0x06A4=>1, 0x06A5=>1, 0x06A6=>1, 0x06A7=>1, 0x06A8=>1,
2485 0x06A9=>1, 0x06AA=>1, 0x06AB=>1, 0x06AC=>1, 0x06AD=>1, 0x06AE=>1, 0x06AF=>1, 0x06B0=>1,
2486 0x06B1=>1, 0x06B2=>1, 0x06B3=>1, 0x06B4=>1, 0x06B5=>1, 0x06B6=>1, 0x06B7=>1, 0x06B8=>1,
2487 0x06B9=>1, 0x06BA=>1, 0x06BB=>1, 0x06BC=>1, 0x06BD=>1, 0x06BE=>1, 0x06BF=>1, 0x06C1=>1,
2488 0x06C2=>1, 0x06CC=>1, 0x06CE=>1, 0x06D0=>1, 0x06D1=>1, 0x06FA=>1, 0x06FB=>1, 0x06FC=>1,
2489 0x06FF=>1,
2490 /* Arabic Supplement */
2491 0x0750=>1, 0x0751=>1, 0x0752=>1, 0x0753=>1, 0x0754=>1, 0x0755=>1, 0x0756=>1, 0x0757=>1,
2492 0x0758=>1, 0x075C=>1, 0x075D=>1, 0x075E=>1, 0x075F=>1, 0x0760=>1, 0x0761=>1, 0x0762=>1,
2493 0x0763=>1, 0x0764=>1, 0x0765=>1, 0x0766=>1, 0x0767=>1, 0x0768=>1, 0x0769=>1, 0x076A=>1,
2494 0x076D=>1, 0x076E=>1, 0x076F=>1, 0x0770=>1, 0x0772=>1, 0x0775=>1, 0x0776=>1, 0x0777=>1,
2495 0x077A=>1, 0x077B=>1, 0x077C=>1, 0x077D=>1, 0x077E=>1, 0x077F=>1,
2496 /* Extended Arabic */
2497 0x08A0=>1, 0x08A2=>1, 0x08A3=>1, 0x08A4=>1, 0x08A5=>1, 0x08A6=>1, 0x08A7=>1, 0x08A8=>1,
2498 0x08A9=>1,
2499 /* 'syrc' Syriac */
2500 0x0712=>1, 0x0713=>1, 0x0714=>1, 0x071A=>1, 0x071B=>1, 0x071C=>1, 0x071D=>1, 0x071F=>1,
2501 0x0720=>1, 0x0721=>1, 0x0722=>1, 0x0723=>1, 0x0724=>1, 0x0725=>1, 0x0726=>1, 0x0727=>1,
2502 0x0729=>1, 0x072B=>1, 0x072D=>1, 0x072E=>1, 0x074E=>1, 0x074F=>1,
2503 /* N'Ko */
2504 0x07CA=>1, 0x07CB=>1, 0x07CC=>1, 0x07CD=>1, 0x07CE=>1, 0x07CF=>1, 0x07D0=>1, 0x07D1=>1,
2505 0x07D2=>1, 0x07D3=>1, 0x07D4=>1, 0x07D5=>1, 0x07D6=>1, 0x07D7=>1, 0x07D8=>1, 0x07D9=>1,
2506 0x07DA=>1, 0x07DB=>1, 0x07DC=>1, 0x07DD=>1, 0x07DE=>1, 0x07DF=>1, 0x07E0=>1, 0x07E1=>1,
2507 0x07E2=>1, 0x07E3=>1, 0x07E4=>1, 0x07E5=>1, 0x07E6=>1, 0x07E7=>1, 0x07E8=>1, 0x07E9=>1,
2508 0x07EA=>1, 0x07FA=>1,
2509 /* Mandaic */
2510 0x0841=>1, 0x0842=>1, 0x0843=>1, 0x0844=>1, 0x0845=>1, 0x0847=>1, 0x0848=>1, 0x084A=>1,
2511 0x084B=>1, 0x084C=>1, 0x084D=>1, 0x084E=>1, 0x0850=>1, 0x0851=>1, 0x0852=>1, 0x0853=>1,
2512 0x0855=>1,
2513 /* ZWJ U+200D */
2514 0x0200D=>1);
2515
2516 /* JOIN TO PREVIOUS LETTER IN LOGICAL ORDER (i.e. AS FINAL/MEDIAL FORM) = Unicode Right-Joining (+ Dual-Joining + Join_Causing) */
2517 $this->arabRightJoining = array(
2518 0x0620=>1, 0x0622=>1, 0x0623=>1, 0x0624=>1, 0x0625=>1, 0x0626=>1, 0x0627=>1, 0x0628=>1,
2519 0x0629=>1, 0x062A=>1, 0x062B=>1, 0x062C=>1, 0x062D=>1, 0x062E=>1, 0x062F=>1, 0x0630=>1,
2520 0x0631=>1, 0x0632=>1, 0x0633=>1, 0x0634=>1, 0x0635=>1, 0x0636=>1, 0x0637=>1, 0x0638=>1,
2521 0x0639=>1, 0x063A=>1, 0x063B=>1, 0x063C=>1, 0x063D=>1, 0x063E=>1, 0x063F=>1, 0x0640=>1,
2522 0x0641=>1, 0x0642=>1, 0x0643=>1, 0x0644=>1, 0x0645=>1, 0x0646=>1, 0x0647=>1, 0x0648=>1,
2523 0x0649=>1, 0x064A=>1, 0x066E=>1, 0x066F=>1, 0x0671=>1, 0x0672=>1, 0x0673=>1, 0x0675=>1,
2524 0x0676=>1, 0x0677=>1, 0x0678=>1, 0x0679=>1, 0x067A=>1, 0x067B=>1, 0x067C=>1, 0x067D=>1,
2525 0x067E=>1, 0x067F=>1, 0x0680=>1, 0x0681=>1, 0x0682=>1, 0x0683=>1, 0x0684=>1, 0x0685=>1,
2526 0x0686=>1, 0x0687=>1, 0x0688=>1, 0x0689=>1, 0x068A=>1, 0x068B=>1, 0x068C=>1, 0x068D=>1,
2527 0x068E=>1, 0x068F=>1, 0x0690=>1, 0x0691=>1, 0x0692=>1, 0x0693=>1, 0x0694=>1, 0x0695=>1,
2528 0x0696=>1, 0x0697=>1, 0x0698=>1, 0x0699=>1, 0x069A=>1, 0x069B=>1, 0x069C=>1, 0x069D=>1,
2529 0x069E=>1, 0x069F=>1, 0x06A0=>1, 0x06A1=>1, 0x06A2=>1, 0x06A3=>1, 0x06A4=>1, 0x06A5=>1,
2530 0x06A6=>1, 0x06A7=>1, 0x06A8=>1, 0x06A9=>1, 0x06AA=>1, 0x06AB=>1, 0x06AC=>1, 0x06AD=>1,
2531 0x06AE=>1, 0x06AF=>1, 0x06B0=>1, 0x06B1=>1, 0x06B2=>1, 0x06B3=>1, 0x06B4=>1, 0x06B5=>1,
2532 0x06B6=>1, 0x06B7=>1, 0x06B8=>1, 0x06B9=>1, 0x06BA=>1, 0x06BB=>1, 0x06BC=>1, 0x06BD=>1,
2533 0x06BE=>1, 0x06BF=>1, 0x06C0=>1, 0x06C1=>1, 0x06C2=>1, 0x06C3=>1, 0x06C4=>1, 0x06C5=>1,
2534 0x06C6=>1, 0x06C7=>1, 0x06C8=>1, 0x06C9=>1, 0x06CA=>1, 0x06CB=>1, 0x06CC=>1, 0x06CD=>1,
2535 0x06CE=>1, 0x06CF=>1, 0x06D0=>1, 0x06D1=>1, 0x06D2=>1, 0x06D3=>1, 0x06D5=>1, 0x06EE=>1,
2536 0x06EF=>1, 0x06FA=>1, 0x06FB=>1, 0x06FC=>1, 0x06FF=>1,
2537 /* Arabic Supplement */
2538 0x0750=>1, 0x0751=>1, 0x0752=>1, 0x0753=>1, 0x0754=>1, 0x0755=>1, 0x0756=>1, 0x0757=>1,
2539 0x0758=>1, 0x0759=>1, 0x075A=>1, 0x075B=>1, 0x075C=>1, 0x075D=>1, 0x075E=>1, 0x075F=>1,
2540 0x0760=>1, 0x0761=>1, 0x0762=>1, 0x0763=>1, 0x0764=>1, 0x0765=>1, 0x0766=>1, 0x0767=>1,
2541 0x0768=>1, 0x0769=>1, 0x076A=>1, 0x076B=>1, 0x076C=>1, 0x076D=>1, 0x076E=>1, 0x076F=>1,
2542 0x0770=>1, 0x0771=>1, 0x0772=>1, 0x0773=>1, 0x0774=>1, 0x0775=>1, 0x0776=>1, 0x0777=>1,
2543 0x0778=>1, 0x0779=>1, 0x077A=>1, 0x077B=>1, 0x077C=>1, 0x077D=>1, 0x077E=>1, 0x077F=>1,
2544 /* Extended Arabic */
2545 0x08A0=>1, 0x08A2=>1, 0x08A3=>1, 0x08A4=>1, 0x08A5=>1, 0x08A6=>1, 0x08A7=>1, 0x08A8=>1,
2546 0x08A9=>1, 0x08AA=>1, 0x08AB=>1, 0x08AC=>1,
2547 /* 'syrc' Syriac */
2548 0x0710=>1, 0x0712=>1, 0x0713=>1, 0x0714=>1, 0x0715=>1, 0x0716=>1, 0x0717=>1, 0x0718=>1,
2549 0x0719=>1, 0x071A=>1, 0x071B=>1, 0x071C=>1, 0x071D=>1, 0x071E=>1, 0x071F=>1, 0x0720=>1,
2550 0x0721=>1, 0x0722=>1, 0x0723=>1, 0x0724=>1, 0x0725=>1, 0x0726=>1, 0x0727=>1, 0x0728=>1,
2551 0x0729=>1, 0x072A=>1, 0x072B=>1, 0x072C=>1, 0x072D=>1, 0x072E=>1, 0x072F=>1, 0x074D=>1,
2552 0x074E=>1, 0x074F,
2553 /* N'Ko */
2554 0x07CA=>1, 0x07CB=>1, 0x07CC=>1, 0x07CD=>1, 0x07CE=>1, 0x07CF=>1, 0x07D0=>1, 0x07D1=>1,
2555 0x07D2=>1, 0x07D3=>1, 0x07D4=>1, 0x07D5=>1, 0x07D6=>1, 0x07D7=>1, 0x07D8=>1, 0x07D9=>1,
2556 0x07DA=>1, 0x07DB=>1, 0x07DC=>1, 0x07DD=>1, 0x07DE=>1, 0x07DF=>1, 0x07E0=>1, 0x07E1=>1,
2557 0x07E2=>1, 0x07E3=>1, 0x07E4=>1, 0x07E5=>1, 0x07E6=>1, 0x07E7=>1, 0x07E8=>1, 0x07E9=>1,
2558 0x07EA=>1, 0x07FA=>1,
2559 /* Mandaic */
2560 0x0841=>1, 0x0842=>1, 0x0843=>1, 0x0844=>1, 0x0845=>1, 0x0847=>1, 0x0848=>1, 0x084A=>1,
2561 0x084B=>1, 0x084C=>1, 0x084D=>1, 0x084E=>1, 0x0850=>1, 0x0851=>1, 0x0852=>1, 0x0853=>1,
2562 0x0855=>1,
2563 0x0840=>1, 0x0846=>1, 0x0849=>1, 0x084F=>1, 0x0854=>1, /* Right joining */
2564 /* ZWJ U+200D */
2565 0x0200D=>1);
2566
2567
2568 /* VOWELS = TRANSPARENT-JOINING = Unicode Transparent-Joining type (not just vowels) */
2569 $this->arabTransparent = array(
2570 0x0610=>1, 0x0611=>1, 0x0612=>1, 0x0613=>1, 0x0614=>1, 0x0615=>1, 0x0616=>1, 0x0617=>1,
2571 0x0618=>1, 0x0619=>1, 0x061A=>1, 0x064B=>1, 0x064C=>1, 0x064D=>1, 0x064E=>1, 0x064F=>1,
2572 0x0650=>1, 0x0651=>1, 0x0652=>1, 0x0653=>1, 0x0654=>1, 0x0655=>1, 0x0656=>1, 0x0657=>1,
2573 0x0658=>1, 0x0659=>1, 0x065A=>1, 0x065B=>1, 0x065C=>1, 0x065D=>1, 0x065E=>1, 0x065F=>1,
2574 0x0670=>1, 0x06D6=>1, 0x06D7=>1, 0x06D8=>1, 0x06D9=>1, 0x06DA=>1, 0x06DB=>1, 0x06DC=>1,
2575 0x06DF=>1, 0x06E0=>1, 0x06E1=>1, 0x06E2=>1, 0x06E3=>1, 0x06E4=>1, 0x06E7=>1, 0x06E8=>1,
2576 0x06EA=>1, 0x06EB=>1, 0x06EC=>1, 0x06ED=>1,
2577 /* Extended Arabic */
2578 0x08E4=>1, 0x08E5=>1, 0x08E6=>1, 0x08E7=>1, 0x08E8=>1, 0x08E9=>1, 0x08EA=>1, 0x08EB=>1,
2579 0x08EC=>1, 0x08ED=>1, 0x08EE=>1, 0x08EF=>1, 0x08F0=>1, 0x08F1=>1, 0x08F2=>1, 0x08F3=>1,
2580 0x08F4=>1, 0x08F5=>1, 0x08F6=>1, 0x08F7=>1, 0x08F8=>1, 0x08F9=>1, 0x08FA=>1, 0x08FB=>1,
2581 0x08FC=>1, 0x08FD=>1, 0x08FE=>1,
2582 /* Arabic ligatures in presentation form (converted in 'ccmp' in e.g. Arial and Times ? need to add others in this range) */
2583 0xFC5E=>1, 0xFC5F=>1, 0xFC60=>1, 0xFC61=>1, 0xFC62=>1,
2584 /* 'syrc' Syriac */
2585 0x070F=>1, 0x0711=>1, 0x0730=>1, 0x0731=>1, 0x0732=>1, 0x0733=>1, 0x0734=>1, 0x0735=>1,
2586 0x0736=>1, 0x0737=>1, 0x0738=>1, 0x0739=>1, 0x073A=>1, 0x073B=>1, 0x073C=>1, 0x073D=>1,
2587 0x073E=>1, 0x073F=>1, 0x0740=>1, 0x0741=>1, 0x0742=>1, 0x0743=>1, 0x0744=>1, 0x0745=>1,
2588 0x0746=>1, 0x0747=>1, 0x0748=>1, 0x0749=>1, 0x074A=>1,
2589 /* N'Ko */
2590 0x07EB=>1, 0x07EC=>1, 0x07ED=>1, 0x07EE=>1, 0x07EF=>1, 0x07F0=>1, 0x07F1=>1, 0x07F2=>1,
2591 0x07F3=>1,
2592 /* Mandaic */
2593 0x0859=>1, 0x085A=>1, 0x085B=>1,
2594 );
2595
2596 }
2597
2598
2599 function arabic_shaper($usetags, $scriptTag) {
2600 $chars = array();
2601 for($i=0;$i<count($this->OTLdata);$i++) {
2602 $chars[] = $this->OTLdata[$i]['hex'];
2603 }
2604 $crntChar = null;
2605 $prevChar = null;
2606 $nextChar = null;
2607 $output = array();
2608 $max = count($chars);
2609 for ($i = $max - 1; $i >= 0; $i--) {
2610 $crntChar = $chars[$i];
2611 if ($i > 0){ $prevChar = hexdec($chars[$i - 1]); }
2612 else{ $prevChar = NULL; }
2613 if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 2]) ) {
2614 $prevChar = hexdec($chars[$i - 2]);
2615 if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 3])) {
2616 $prevChar = hexdec($chars[$i - 3]);
2617 if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 4])) {
2618 $prevChar = hexdec($chars[$i - 4]);
2619 }
2620 }
2621 }
2622 if ($crntChar && isset($this->arabTransparentJoin[ hexdec($crntChar)]) ) {
2623 // If next_char = RightJoining && prev_char = LeftJoining:
2624 if (isset($chars[$i + 1]) && $chars[$i + 1] && isset($this->arabRightJoining[ hexdec($chars[$i + 1]) ]) && $prevChar && isset($this->arabLeftJoining[$prevChar])) {
2625 $output[] = $this->get_arab_glyphs($crntChar, 1, $chars, $i, $scriptTag, $usetags); // <final> form
2626 }
2627 else {
2628 $output[] = $this->get_arab_glyphs($crntChar, 0, $chars, $i, $scriptTag, $usetags); // <isolated> form
2629 }
2630 continue;
2631 }
2632 if (hexdec($crntChar) < 128) {
2633 $output[] = array($crntChar,0);
2634 $nextChar = $crntChar;
2635 continue;
2636 }
2637 // 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2638 $form = 0;
2639 if ($prevChar && isset($this->arabLeftJoining[$prevChar])) {
2640 $form++;
2641 }
2642 if ($nextChar && isset($this->arabRightJoining[ hexdec($nextChar) ])) {
2643 $form += 2;
2644 }
2645 $output[] = $this->get_arab_glyphs($crntChar, $form, $chars, $i, $scriptTag, $usetags) ;
2646 $nextChar = $crntChar;
2647 }
2648 $ra = array_reverse($output);
2649 for($i=0;$i<count($this->OTLdata);$i++) {
2650 $this->OTLdata[$i]['uni'] = hexdec($ra[$i][0]);
2651 $this->OTLdata[$i]['hex'] = $ra[$i][0];
2652 $this->OTLdata[$i]['form'] = $ra[$i][1]; // Actaul form substituted 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2653 }
2654 }
2655
2656
2657 function get_arab_glyphs($char, $type, &$chars, $i, $scriptTag, $usetags) {
2658
2659 // Optional Feature settings // doesn't control Syriac at present
2660 if (($type===0 && strpos($usetags, 'isol')===false) || ($type===1 && strpos($usetags, 'fina')===false) || ($type===2 && strpos($usetags, 'init')===false) || ($type===3 && strpos($usetags, 'medi')===false)) {
2661 return array($char,0);
2662 }
2663
2664 // 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL (:: 4=MED2 :: 5=FIN2 :: 6=FIN3)
2665 $retk = -1;
2666 // Alaph 00710 in Syriac
2667 if ($scriptTag=='syrc' && $char=='00710') {
2668 // if there is a preceding (base?) character *** should search back to previous base - ignoring vowels and change $n
2669 // set $n as the position of the last base; for now we'll just do this:
2670 $n = $i-1;
2671 // if the preceding (base) character cannot be joined to
2672 // not in $this->arabLeftJoining i.e. not a char which can join to the next one
2673 if (isset($chars[$n]) && isset($this->arabLeftJoining[hexdec($chars[$n])])) {
2674 // if in the middle of Syriac words
2675 if (isset($chars[$i+1]) && preg_match('/[\x{0700}-\x{0745}]/u',code2utf(hexdec($chars[$n]))) && preg_match('/[\x{0700}-\x{0745}]/u',code2utf(hexdec($chars[$i+1]))) && isset($this->arabGlyphs[$char][4])) { $retk = 4; }
2676 // if at the end of Syriac words
2677 else if(!isset($chars[$i+1]) || !preg_match('/[\x{0700}-\x{0745}]/u',code2utf(hexdec($chars[$i+1])))) {
2678 // if preceding base character IS (00715|00716|0072A)
2679 if (strpos('0715|0716|072A',$chars[$n])!==false && isset($this->arabGlyphs[$char][6])) { $retk = 6; }
2680
2681 // else if preceding base character is NOT (00715|00716|0072A)
2682 else if (isset($this->arabGlyphs[$char][5])) { $retk = 5; }
2683 }
2684 }
2685 if ($retk != -1) {
2686 return array($this->arabGlyphs[$char][$retk],$retk);
2687 }
2688 else { return array($char,0); }
2689 }
2690
2691 if (($type>0 || $type===0) && isset($this->arabGlyphs[$char][$type])) {
2692 $retk = $type;
2693 }
2694 else if ($type==3 && isset($this->arabGlyphs[$char][1])) { // if <medial> not defined, but <final>, return <final>
2695 $retk = 1;
2696 }
2697 else if ($type==2 && isset($this->arabGlyphs[$char][0])) { // if <initial> not defined, but <isolated>, return <isolated>
2698 $retk = 0;
2699 }
2700 if ($retk != -1) {
2701 $match = true;
2702 // If GSUB includes a Backtrack or Lookahead condition (e.g. font ArabicTypesetting)
2703 if (isset($this->arabGlyphs[$char]['prel'][$retk]) && $this->arabGlyphs[$char]['prel'][$retk]) {
2704 $ig = 1;
2705 foreach($this->arabGlyphs[$char]['prel'][$retk] AS $k=>$v) { // $k starts 0, 1...
2706 if (!isset($chars[$i-$ig-$k])) { $match = false; }
2707 else if (strpos($v,$chars[$i-$ig-$k])===false) {
2708 while (strpos($this->arabGlyphs[$char]['ignore'][$retk],$chars[$i-$ig-$k])!==false) { // ignore
2709 $ig++;
2710 }
2711 if (!isset($chars[$i-$ig-$k])) { $match = false; }
2712 else if (strpos($v,$chars[$i-$ig-$k])===false) { $match = false; }
2713 }
2714 }
2715 }
2716 if (isset($this->arabGlyphs[$char]['postl'][$retk]) && $this->arabGlyphs[$char]['postl'][$retk]) {
2717 $ig = 1;
2718 foreach($this->arabGlyphs[$char]['postl'][$retk] AS $k=>$v) { // $k starts 0, 1...
2719 if (!isset($chars[$i+$ig+$k])) { $match = false; }
2720 else if (strpos($v,$chars[$i+$ig+$k])===false) {
2721 while (strpos($this->arabGlyphs[$char]['ignore'][$retk],$chars[$i+$ig+$k])!==false) { // ignore
2722 $ig++;
2723 }
2724 if (!isset($chars[$i+$ig+$k])) { $match = false; }
2725 else if (strpos($v,$chars[$i+$ig+$k])===false) { $match = false; }
2726 }
2727 }
2728 }
2729 if ($match) {
2730 return array($this->arabGlyphs[$char][$retk],$retk);
2731 }
2732 else { return array($char,0); }
2733 }
2734 else { return array($char,0); }
2735 }
2736
2737
2738 ////////////////////////////////////////////////////////////////
2739 ////////////////////////////////////////////////////////////////
2740 ///////////////// LINE BREAKING ///////////////////////
2741 ////////////////////////////////////////////////////////////////
2742 ////////////////////////////////////////////////////////////////
2743
2744 ////////////////////////////////////////////////////////////////
2745 ///////////// TIBETAN LINE BREAKING ///////////////////
2746 ////////////////////////////////////////////////////////////////
2747 // Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
2748 function TibetanlineBreaking() {
2749 for ($ptr=0; $ptr<count($this->OTLdata);$ptr++) {
2750 // Break opportunities at U+0F0B Tsheg or U=0F0D
2751 if (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni']==0x0F0B || $this->OTLdata[$ptr]['uni']==0x0F0D)) {
2752 if (isset($this->OTLdata[$ptr+1]['uni']) && ($this->OTLdata[$ptr+1]['uni']==0x0F0D || $this->OTLdata[$ptr+1]['uni']==0xF0E)) { continue; }
2753 // Set end of word marker in OTLdata at matchpos
2754 $this->OTLdata[$ptr]['wordend'] = true;
2755 }
2756 }
2757
2758 }
2759
2760 ////////////////////////////////////////////////////////////////
2761 ////////// SOUTH EAST ASIAN LINE BREAKING /////////////
2762 ////////////////////////////////////////////////////////////////
2763 // South East Asian Linebreaking (Thai, Khmer and Lao) using dictionary of words
2764 // Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
2765 function SEAlineBreaking() {
2766 // Load Line-breaking dictionary
2767 if (!isset($this->lbdicts[$this->shaper]) && file_exists(_MPDF_PATH.'includes/linebrdict'.$this->shaper.'.dat')) { $this->lbdicts[$this->shaper] = file_get_contents(_MPDF_PATH.'includes/linebrdict'.$this->shaper.'.dat'); }
2768
2769 $dict =&$this->lbdicts[$this->shaper];
2770
2771 // Find all word boundaries and mark end of word $this->OTLdata[$i]['wordend']=true on last character
2772 // If Thai, allow for possible suffixes (not in Lao or Khmer)
2773
2774 // repeater/ellision characters
2775 // (0x0E2F); // Ellision character THAI_PAIYANNOI 0x0E2F UTF-8 0xE0 0xB8 0xAF
2776 // (0x0E46); // Repeat character THAI_MAIYAMOK 0x0E46 UTF-8 0xE0 0xB9 0x86
2777 // (0x0EC6); // Repeat character LAO UTF-8 0xE0 0xBB 0x86
2778
2779 $rollover = array();
2780 $ptr = 0;
2781 while ($ptr<count($this->OTLdata)-3) {
2782 if (count($rollover)) {
2783 $matches = $rollover;
2784 $rollover = array();
2785 }
2786 else {
2787 $matches = $this->checkwordmatch($dict, $ptr);
2788 }
2789 if (count($matches)==1) {
2790 $matchpos = $matches[0];
2791 // Check for repeaters - if so $matchpos++
2792 if (isset($this->OTLdata[$matchpos+1]['uni']) && ($this->OTLdata[$matchpos+1]['uni']==0x0E2F || $this->OTLdata[$matchpos+1]['uni']==0x0E46 || $this->OTLdata[$matchpos+1]['uni']==0x0EC6)) { $matchpos++; }
2793 // Set end of word marker in OTLdata at matchpos
2794 $this->OTLdata[$matchpos]['wordend'] = true;
2795 $ptr = $matchpos + 1;
2796 }
2797 else if (empty($matches)) {
2798 $ptr++;
2799 // Move past any ASCII characters
2800 while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) { $ptr++; }
2801 }
2802 else { // Multiple matches
2803 $secondmatch = false;
2804 for ($m=count($matches)-1;$m>=0;$m--) {
2805 //for ($m=0;$m<count($matches);$m++) {
2806 $firstmatch = $matches[$m];
2807 $matches2 = $this->checkwordmatch($dict, $firstmatch+1);
2808 if (count($matches2)) {
2809 // Set end of word marker in OTLdata at matchpos
2810 $this->OTLdata[$firstmatch]['wordend'] = true;
2811 $ptr = $firstmatch + 1;
2812 $rollover = $matches2;
2813 $secondmatch = true;
2814 break;
2815 }
2816 }
2817 if (!$secondmatch) {
2818 // Set end of word marker in OTLdata at end of longest first match
2819 $this->OTLdata[$matches[count($matches)-1]]['wordend'] = true;
2820 $ptr = $matches[count($matches)-1] + 1;
2821 // Move past any ASCII characters
2822 while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) { $ptr++; }
2823 }
2824 }
2825
2826 }
2827
2828 }
2829
2830 function checkwordmatch(&$dict, $ptr) {
2831 /*
2832 define("_DICT_NODE_TYPE_SPLIT", 0x01);
2833 define("_DICT_NODE_TYPE_LINEAR", 0x02);
2834 define("_DICT_INTERMEDIATE_MATCH", 0x03);
2835 define("_DICT_FINAL_MATCH", 0x04);
2836
2837 Node type: Split.
2838 Divide at < 98 >= 98
2839 Offset for >= 98 == 79 (long 4-byte unsigned)
2840
2841 Node type: Linear match.
2842 Char = 97
2843
2844 Intermediate match
2845
2846 Final match
2847 */
2848
2849 $dictptr = 0;
2850 $ok = true;
2851 $matches = array();
2852 while ($ok) {
2853 $x = ord($dict{$dictptr});
2854 $c = $this->OTLdata[$ptr]['uni'] & 0xFF;
2855 if ($x==_DICT_INTERMEDIATE_MATCH) {
2856 //echo "DICT_INTERMEDIATE_MATCH: ".dechex($c).'<br />';
2857 // Do not match if next character in text is a Mark
2858 if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex'])===false) {
2859 $matches[] = $ptr - 1;
2860 }
2861 $dictptr++;
2862 }
2863 else if ($x==_DICT_FINAL_MATCH) {
2864 //echo "DICT_FINAL_MATCH: ".dechex($c).'<br />';
2865 // Do not match if next character in text is a Mark
2866 if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex'])===false) {
2867 $matches[] = $ptr - 1;
2868 }
2869 return $matches;
2870 }
2871 else if ($x==_DICT_NODE_TYPE_LINEAR) {
2872 //echo "DICT_NODE_TYPE_LINEAR: ".dechex($c).'<br />';
2873 $dictptr++;
2874 $m = ord($dict{$dictptr});
2875 if ($c == $m) {
2876 $ptr++;
2877 if ($ptr > count($this->OTLdata)-1) {
2878 $next = ord($dict{$dictptr+1});
2879 if ($next==_DICT_INTERMEDIATE_MATCH || $next==_DICT_FINAL_MATCH) {
2880 // Do not match if next character in text is a Mark
2881 if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex'])===false) {
2882 $matches[] = $ptr - 1;
2883 }
2884 }
2885 return $matches;
2886 }
2887 $dictptr++;
2888 continue;
2889 }
2890 else {
2891 //echo "DICT_NODE_TYPE_LINEAR NOT: ".dechex($c).'<br />';
2892 return $matches;
2893 }
2894 }
2895 else if ($x==_DICT_NODE_TYPE_SPLIT) {
2896 //echo "DICT_NODE_TYPE_SPLIT ON ".dechex($d).": ".dechex($c).'<br />';
2897 $dictptr++;
2898 $d = ord($dict{$dictptr});
2899 if ($c < $d) {
2900 $dictptr += 5;
2901 }
2902 else {
2903 $dictptr++;
2904 // Unsigned long 32-bit offset
2905 $offset = (ord($dict{$dictptr})*16777216) + (ord($dict{$dictptr+1})<<16) + (ord($dict{$dictptr+2})<<8) + ord($dict{$dictptr+3});
2906 $dictptr = $offset;
2907 }
2908 }
2909 else {
2910 //echo "PROBLEM: ".($x).'<br />';
2911 $ok = false; // Something has gone wrong
2912 }
2913 }
2914
2915 return $matches;
2916 }
2917
2918
2919
2920 ////////////////////////////////////////////////////////////////
2921 ////////////////////////////////////////////////////////////////
2922 ////////// GPOS ///////////////////////////////////////
2923 ////////////////////////////////////////////////////////////////
2924 ////////////////////////////////////////////////////////////////
2925
2926 function _applyGPOSrules($LookupList, $is_old_spec=false) {
2927 foreach($LookupList AS $lu=>$tag) {
2928 $Type = $this->GPOSLookups[$lu]['Type'];
2929 $Flag = $this->GPOSLookups[$lu]['Flag'];
2930 $MarkFilteringSet = '';
2931 if (isset($this->GPOSLookups[$lu]['MarkFilteringSet']))
2932 $MarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
2933 $ptr = 0;
2934 // Test each glyph sequentially
2935 while($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
2936 $currGlyph = $this->OTLdata[$ptr]['hex'];
2937 $currGID = $this->OTLdata[$ptr]['uni'];
2938 $shift = 1;
2939 foreach($this->GPOSLookups[$lu]['Subtables'] AS $c=>$subtable_offset) {
2940 // NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
2941 if (isset($this->LuCoverage[$lu][$c][$currGID])) {
2942 // Get rules from font GPOS subtable
2943 if (isset($this->OTLdata[$ptr]['bidi_type'])) { // No need to check bidi_type - just a check that it exists
2944 $shift = $this->_applyGPOSsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GPOS_offset + $this->GSUB_length), $Type, $Flag, $MarkFilteringSet, $this->LuCoverage[$lu][$c], $tag, 0, $is_old_spec);
2945 if ($shift) { break; }
2946 }
2947 }
2948 }
2949 if ($shift == 0) { $shift = 1; }
2950 $ptr += $shift;
2951
2952 }
2953 }
2954 }
2955
2956 //////////////////////////////////////////////////////////////////////////////////
2957 // GPOS Types
2958 // Lookup Type 1: Single Adjustment Positioning Subtable Adjust position of a single glyph
2959 // Lookup Type 2: Pair Adjustment Positioning Subtable Adjust position of a pair of glyphs
2960 // Lookup Type 3: Cursive Attachment Positioning Subtable Attach cursive glyphs
2961 // Lookup Type 4: MarkToBase Attachment Positioning Subtable Attach a combining mark to a base glyph
2962 // Lookup Type 5: MarkToLigature Attachment Positioning Subtable Attach a combining mark to a ligature
2963 // Lookup Type 6: MarkToMark Attachment Positioning Subtable Attach a combining mark to another mark
2964 // Lookup Type 7: Contextual Positioning Subtables Position one or more glyphs in context
2965 // Lookup Type 8: Chaining Contextual Positioning Subtable Position one or more glyphs in chained context
2966 // Lookup Type 9: Extension positioning
2967 //////////////////////////////////////////////////////////////////////////////////
2968 function _applyGPOSvaluerecord($basepos,$Value) {
2969
2970 // If current glyph is a mark with a defined width, any XAdvance is considered to REPLACE the character Advance Width
2971 // Test case <div style="font-family:myanmartext">&#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;</div>
2972 if (strpos($this->GlyphClassMarks, $this->OTLdata[$basepos]['hex'])!==false) {
2973 $cw = round($this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$basepos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000); // convert back to font design units
2974 }
2975 else {
2976 $cw = 0;
2977 }
2978
2979 $apos = $this->_getXAdvancePos($basepos);
2980
2981 if (isset($Value['XAdvance']) && ($Value['XAdvance']-$cw) != 0) {
2982 // However DON'T REPLACE the character Advance Width if Advance Width is negative
2983 // Test case <div style="font-family: dejavusansmono">&#x440;&#x443;&#x301;&#x441;&#x441;&#x43a;&#x438;&#x439;</div>
2984 if ($Value['XAdvance'] < 0) { $cw = 0; }
2985
2986 // For LTR apply XAdvanceL to the last mark following the base = at $apos
2987 // For RTL apply XAdvanceR to base = at $basepos
2988 if (isset($this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'])) { $this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] += $Value['XAdvance']-$cw; }
2989 else { $this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] = $Value['XAdvance']-$cw; }
2990 if (isset($this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'])) { $this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] += $Value['XAdvance']-$cw; }
2991 else { $this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] = $Value['XAdvance']-$cw; }
2992 }
2993
2994 // Any XPlacement (? and Y Placement) apply to base and marks (from basepos to apos)
2995 for ($a=$basepos;$a<=$apos;$a++) {
2996 if (isset($Value['XPlacement'])) {
2997 if (isset($this->OTLdata[$a]['GPOSinfo']['XPlacement'])) { $this->OTLdata[$a]['GPOSinfo']['XPlacement'] += $Value['XPlacement']; }
2998 else { $this->OTLdata[$a]['GPOSinfo']['XPlacement'] = $Value['XPlacement']; }
2999 }
3000 if (isset($Value['YPlacement'])) {
3001 if (isset($this->OTLdata[$a]['GPOSinfo']['YPlacement'])) { $this->OTLdata[$a]['GPOSinfo']['YPlacement'] += $Value['YPlacement']; }
3002 else { $this->OTLdata[$a]['GPOSinfo']['YPlacement'] = $Value['YPlacement']; }
3003 }
3004 }
3005 }
3006
3007
3008
3009 // If XAdvance is aplied to $ptr - in order for PDF to position the Advance correctly need to place it on
3010 // the last of any Marks which immediately follow the current glyph
3011 function _getXAdvancePos($pos) {
3012 // NB Not all fonts have all marks specified in GlyphClassMarks
3013
3014 // If the current glyph is not a base (but a mark) then ignore this, and apply to the current position
3015 if (strpos($this->GlyphClassMarks, $this->OTLdata[$pos]['hex'])!==false) { return $pos; }
3016
3017 while(isset($this->OTLdata[$pos+1]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$pos+1]['hex'])!==false) { $pos++; }
3018 return $pos ;
3019 }
3020
3021
3022
3023
3024 function _applyGPOSsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $tag, $level=0, $is_old_spec) {
3025 if (($Flag & 0x0001) == 1) { $dir = 'RTL'; } // only used for Type 3
3026 else { $dir = 'LTR'; }
3027 $ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
3028
3029 // Lets start
3030 $this->seek($subtable_offset);
3031 $PosFormat = $this->read_ushort();
3032
3033 ////////////////////////////////////////////////////////////////////////////////
3034 // LookupType 1: Single adjustment Adjust position of a single glyph (e.g. SmallCaps/Sups/Subs)
3035 ////////////////////////////////////////////////////////////////////////////////
3036 if ($Type == 1) {
3037 //===========
3038 // Format 1:
3039 //===========
3040 if ($PosFormat==1) {
3041 $Coverage = $subtable_offset + $this->read_ushort();
3042 $ValueFormat = $this->read_ushort();
3043 $Value = $this->_getValueRecord($ValueFormat);
3044 }
3045 //===========
3046 // Format 2:
3047 //===========
3048 else if ($PosFormat==2) {
3049 $Coverage = $subtable_offset + $this->read_ushort();
3050 $ValueFormat = $this->read_ushort();
3051 $ValueCount = $this->read_ushort();
3052 $GlyphPos = $LuCoverage[$currGID];
3053 $this->skip($GlyphPos * 2 * $this->count_bits($ValueFormat) );
3054 $Value = $this->_getValueRecord($ValueFormat);
3055 }
3056 $this->_applyGPOSvaluerecord($ptr,$Value);
3057 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3058 return 1;
3059 }
3060
3061 ////////////////////////////////////////////////////////////////////////////////
3062 // LookupType 2: Pair adjustment Adjust position of a pair of glyphs (Kerning)
3063 ////////////////////////////////////////////////////////////////////////////////
3064 else if ($Type == 2) {
3065 $Coverage = $subtable_offset + $this->read_ushort();
3066 $ValueFormat1 = $this->read_ushort();
3067 $ValueFormat2 = $this->read_ushort();
3068 $sizeOfPair = ( 2*$this->count_bits($ValueFormat1) ) + ( 2*$this->count_bits($ValueFormat2) );
3069 //===========
3070 // Format 1:
3071 //===========
3072 if ($PosFormat==1) {
3073 $PairSetCount = $this->read_ushort();
3074 $PairSetOffset = array();
3075 for($p=0;$p<$PairSetCount;$p++) {
3076 $PairSetOffset[] = $subtable_offset + $this->read_ushort();
3077 }
3078 for($p=0;$p<$PairSetCount;$p++) {
3079 if (isset($LuCoverage[$currGID]) && $LuCoverage[$currGID]==$p) {
3080 $this->seek($PairSetOffset[$p]);
3081 //PairSet table
3082 $PairValueCount = $this->read_ushort();
3083 for($pv=0;$pv<$PairValueCount;$pv++) {
3084 //PairValueRecord
3085 $gid = $this->read_ushort();
3086 $SecondGlyph = $this->glyphToChar($gid);
3087 $FirstGlyph = $this->OTLdata[$ptr]['uni'];
3088
3089 $checkpos = $ptr;
3090 $checkpos++;
3091 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) {
3092 $checkpos++;
3093 }
3094 if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni']==$SecondGlyph) {
3095 $matchedpos = $checkpos;
3096 }
3097 else { $matchedpos = false; }
3098
3099 if ($matchedpos !== false) {
3100 $Value1 = $this->_getValueRecord($ValueFormat1);
3101 $Value2 = $this->_getValueRecord($ValueFormat2);
3102 if($ValueFormat1) {
3103 $this->_applyGPOSvaluerecord($ptr,$Value1);
3104 }
3105 if($ValueFormat2) {
3106 $this->_applyGPOSvaluerecord($matchedpos,$Value2);
3107 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3108 return $matchedpos - $ptr +1;
3109 }
3110 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3111 return $matchedpos - $ptr;
3112 }
3113 else {
3114 $this->skip($sizeOfPair);
3115 }
3116 }
3117 }
3118 }
3119 return 0;
3120 }
3121 //===========
3122 // Format 2:
3123 //===========
3124 else if ($PosFormat==2) {
3125 $ClassDef1 = $subtable_offset + $this->read_ushort();
3126 $ClassDef2 = $subtable_offset + $this->read_ushort();
3127 $Class1Count = $this->read_ushort();
3128 $Class2Count = $this->read_ushort();
3129
3130 $sizeOfValueRecords = $Class1Count * $Class2Count * $sizeOfPair;
3131
3132 //$this->skip($sizeOfValueRecords ); ???? NOT NEEDED
3133
3134 // NB Class1Count includes Class 0 even though it is not defined by $ClassDef1
3135 // i.e. Class1Count = 5; Class1 will contain array(indices 1-4);
3136 $Class1 = $this->_getClassDefinitionTable($ClassDef1);
3137 $Class2 = $this->_getClassDefinitionTable($ClassDef2);
3138 $FirstGlyph = $this->OTLdata[$ptr]['uni'];
3139 $checkpos = $ptr;
3140 $checkpos++;
3141 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) {
3142 $checkpos++;
3143 }
3144 if (isset($this->OTLdata[$checkpos])) { $matchedpos = $checkpos; }
3145 else { return 0; }
3146
3147 $SecondGlyph = $this->OTLdata[$matchedpos]['uni'];
3148 for($i=0;$i<$Class1Count;$i++) {
3149 if (isset($Class1[$i]) && count($Class1[$i])) {
3150 $FirstClassPos = array_search($FirstGlyph, $Class1[$i]);
3151 if ($FirstClassPos === false) { continue; }
3152 else {
3153 for($j=0;$j<$Class2Count;$j++) {
3154 if (isset($Class2[$j]) && count($Class2[$j])) {
3155
3156 $SecondClassPos = array_search($SecondGlyph, $Class2[$j]);
3157 if ($SecondClassPos === false) { continue; }
3158
3159 // Get ValueRecord[$i][$j]
3160 $offs = ($i*$Class2Count*$sizeOfPair) + ($j*$sizeOfPair);
3161 $this->seek($subtable_offset + 16 + $offs);
3162
3163 $Value1 = $this->_getValueRecord($ValueFormat1);
3164 $Value2 = $this->_getValueRecord($ValueFormat2);
3165 if($ValueFormat1) {
3166 $this->_applyGPOSvaluerecord($ptr,$Value1);
3167 }
3168 if($ValueFormat2) {
3169 $this->_applyGPOSvaluerecord($matchedpos,$Value2);
3170 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3171 return $matchedpos - $ptr +1;
3172 }
3173 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3174 return $matchedpos - $ptr;
3175 }
3176 }
3177 }
3178
3179 }
3180 }
3181 return 0;
3182 }
3183 }
3184
3185 ////////////////////////////////////////////////////////////////////////////////
3186 // LookupType 3: Cursive attachment Attach cursive glyphs
3187 ////////////////////////////////////////////////////////////////////////////////
3188 else if ($Type == 3) {
3189 $this->skip(4);
3190 // Need default XAdvance for glyph
3191 $pdfWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], hexdec($currGlyph)); // DON'T convert back to design units
3192
3193 $CPos = $LuCoverage[$currGID];
3194 $this->skip($CPos * 4);
3195 $EntryAnchor = $this->read_ushort();
3196 $ExitAnchor = $this->read_ushort();
3197 if ($EntryAnchor != 0) {
3198 $EntryAnchor += $subtable_offset;
3199 list($x,$y) = $this->_getAnchorTable($EntryAnchor);
3200 if ($dir == 'RTL') {
3201 if (round($pdfWidth) == round($x * 1000/ $this->mpdf->CurrentFont['unitsPerEm']) ) {
3202 $x = 0;
3203 }
3204 else { $x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm']/1000); }
3205 }
3206
3207 $this->Entry[$ptr] = array('X'=>$x, 'Y'=>$y, 'dir'=>$dir);
3208 }
3209 if ($ExitAnchor != 0) {
3210 $ExitAnchor += $subtable_offset;
3211 list($x,$y) = $this->_getAnchorTable($ExitAnchor);
3212 if ($dir == 'LTR') {
3213 if (round($pdfWidth) == round($x * 1000/ $this->mpdf->CurrentFont['unitsPerEm']) ) {
3214 $x = 0;
3215 }
3216 else { $x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm']/1000); }
3217 }
3218 $this->Exit[$ptr] = array('X'=>$x, 'Y'=>$y, 'dir'=>$dir);
3219 }
3220 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3221 return 1;
3222 }
3223
3224 ////////////////////////////////////////////////////////////////////////////////
3225 // LookupType 4: MarkToBase attachment Attach a combining mark to a base glyph
3226 ////////////////////////////////////////////////////////////////////////////////
3227 else if ($Type == 4) {
3228 $MarkCoverage = $subtable_offset + $this->read_ushort();
3229 //$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3230 $BaseCoverage = $subtable_offset + $this->read_ushort();
3231 $ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3232 $MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3233 $BaseArray = $subtable_offset + $this->read_ushort(); // Offset to BaseArray table
3234
3235 $this->seek($BaseCoverage);
3236 $BaseGlyphs = implode('|',$this->_getCoverage());
3237
3238 $checkpos = $ptr;
3239 $checkpos--;
3240
3241 // ZZZ93
3242 // In Lohit-Kannada font (old-spec), rules specify a Type 4 GPOS to attach below-forms to base glyph
3243 // the repositioning does not happen in MS Word, and shouldn't happen comparing with other fonts
3244 // ?Why not
3245 // This Fix blocks the GPOS rule if the "mark" is not actually classified as a mark in the GlyphClasses of GDEF
3246 // but only in Indic old-spec.
3247 // Test cases: &#xca8;&#xccd;&#xca8;&#xcc1; and &#xc95;&#xccd;&#xcb0;&#xccc;
3248 if ($this->shaper=='I' && $is_old_spec && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex'])===false) { return; }
3249
3250
3251 // "To identify the base glyph that combines with a mark, the text-processing client must look backward in the glyph string from the mark to the preceding base glyph."
3252 while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex'])!==false) {
3253 $checkpos--;
3254 }
3255
3256 if (isset($this->OTLdata[$checkpos]) && strpos($BaseGlyphs, $this->OTLdata[$checkpos]['hex'])!==false) {
3257 $matchedpos = $checkpos;
3258 }
3259 else { $matchedpos = false; }
3260
3261 if ($matchedpos !== false) {
3262
3263 // Get the relevant MarkRecord
3264 $MarkPos = $LuCoverage[$currGID];
3265 $MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3266 //Mark Class is = $MarkRecord['Class']
3267
3268 // Get the relevant BaseRecord
3269 $this->seek($BaseArray);
3270 $BaseCount = $this->read_ushort();
3271 $BasePos = strpos($BaseGlyphs, $this->OTLdata[$matchedpos]['hex'])/6;
3272
3273 // Move to the BaseRecord we want
3274 $nSkip = (2 * $BasePos * $ClassCount );
3275 $this->skip($nSkip);
3276
3277 // Read BaseRecord we want for appropriate Class
3278 $nSkip = 2*$MarkRecord['Class'];
3279 $this->skip($nSkip);
3280 $BaseRecordOffset = $BaseArray + $this->read_ushort();
3281 list($x,$y) = $this->_getAnchorTable($BaseRecordOffset);
3282 $BaseRecord = array('AnchorX'=>$x, 'AnchorY'=>$y); // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3283
3284 // Need default XAdvance for Base glyph
3285 $BaseWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3286 $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $BaseWidth;
3287 // And any intervening (ignored) characters
3288 if (($ptr - $matchedpos) > 1) {
3289 for ($i=$matchedpos+1; $i<$ptr; $i++) {
3290 $BaseWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3291 $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $BaseWidthExtra;
3292
3293 }
3294 }
3295
3296 // Align to previous Glyph by attachment - so need to add to previous placement values
3297 $prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3298 $prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3299
3300 $this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $BaseRecord['AnchorX'] - $MarkRecord['AnchorX'];
3301 $this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $BaseRecord['AnchorY'] - $MarkRecord['AnchorY'];
3302 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3303 return 1;
3304
3305 }
3306 return 0;
3307 }
3308
3309 ////////////////////////////////////////////////////////////////////////////////
3310 // LookupType 5: MarkToLigature attachment Attach a combining mark to a ligature
3311 ////////////////////////////////////////////////////////////////////////////////
3312 else if ($Type == 5) {
3313 $MarkCoverage = $subtable_offset + $this->read_ushort();
3314 //$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3315 $LigatureCoverage = $subtable_offset + $this->read_ushort();
3316 $ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3317 $MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3318 $LigatureArray = $subtable_offset + $this->read_ushort(); // Offset to LigatureArray table
3319
3320 $this->seek($LigatureCoverage);
3321 $LigatureGlyphs = implode('|',$this->_getCoverage());
3322
3323
3324 $checkpos = $ptr;
3325 $checkpos--;
3326
3327 // "To position a combining mark using a MarkToLigature attachment subtable, the text-processing client must work backward from the mark to the preceding ligature glyph."
3328 while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex'])!==false) {
3329 $checkpos--;
3330 }
3331
3332 if (isset($this->OTLdata[$checkpos]) && strpos($LigatureGlyphs, $this->OTLdata[$checkpos]['hex'])!==false) {
3333 $matchedpos = $checkpos;
3334 }
3335 else { $matchedpos = false; }
3336
3337 if ($matchedpos !== false) {
3338
3339 // Get the relevant MarkRecord
3340 $MarkPos = $LuCoverage[$currGID];
3341 $MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3342 //Mark Class is = $MarkRecord['Class']
3343
3344
3345 // Get the relevant LigatureRecord
3346 $this->seek($LigatureArray);
3347 $LigatureCount = $this->read_ushort();
3348 $LigaturePos = strpos($LigatureGlyphs, $this->OTLdata[$matchedpos]['hex'])/6;
3349
3350 // Move to the LigatureAttach table Record we want
3351 $nSkip = (2 * $LigaturePos);
3352 $this->skip($nSkip);
3353 $LigatureAttachOffset = $LigatureArray + $this->read_ushort();
3354 $this->seek($LigatureAttachOffset);
3355 $ComponentCount = $this->read_ushort();
3356 $offsets = array();
3357 for ($comp=0;$comp<$ComponentCount;$comp++) {
3358 // ComponentRecords
3359 for ($class=0;$class<$ClassCount;$class++) {
3360 $offsets[$comp][$class] = $this->read_ushort();
3361 }
3362 }
3363
3364 // Get the specific component for this mark attachment
3365 if (isset($this->assocLigs[$matchedpos]) && isset($this->assocMarks[$ptr]['ligPos']) && $this->assocMarks[$ptr]['ligPos']==$matchedpos) {
3366 $component = $this->assocMarks[$ptr]['compID'] ;
3367 }
3368 else { $component = $ComponentCount-1; }
3369
3370 $offset = $offsets[$component][$MarkRecord['Class']];
3371 if ($offset!=0) {
3372 $LigatureRecordOffset = $offset + $LigatureAttachOffset;
3373 list($x,$y) = $this->_getAnchorTable($LigatureRecordOffset);
3374 $LigatureRecord = array('AnchorX'=>$x, 'AnchorY'=>$y);
3375
3376 // Need default XAdvance for Ligature glyph
3377 $LigatureWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3378 $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $LigatureWidth;
3379 // And any intervening (ignored)characters
3380 if (($ptr - $matchedpos) > 1) {
3381 for ($i=$matchedpos+1; $i<$ptr; $i++) {
3382 $LigatureWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3383 $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $LigatureWidthExtra;
3384
3385 }
3386 }
3387
3388 // Align to previous Ligature by attachment - so need to add to previous placement values
3389 if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'])) $prevXPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'];
3390 else { $prevXPlacement = 0; }
3391 if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'])) { $prevYPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']; }
3392 else { $prevYPlacement = 0; }
3393
3394 $this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $LigatureRecord['AnchorX'] - $MarkRecord['AnchorX'];
3395 $this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $LigatureRecord['AnchorY'] - $MarkRecord['AnchorY'];
3396 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3397 return 1;
3398
3399 }
3400 }
3401 return 0;
3402 }
3403
3404 ////////////////////////////////////////////////////////////////////////////////
3405 // LookupType 6: MarkToMark attachment Attach a combining mark to another mark
3406 ////////////////////////////////////////////////////////////////////////////////
3407 else if ($Type == 6) {
3408 $Mark1Coverage = $subtable_offset + $this->read_ushort(); // Combining Mark
3409 //$Mark1Coverage is already set in $LuCoverage 0065|0073 etc
3410 $Mark2Coverage = $subtable_offset + $this->read_ushort(); // Base Mark
3411 $ClassCount = $this->read_ushort(); // Number of classes defined for marks = No. of Combining mark1 glyphs in the MarkCoverage table
3412 $Mark1Array = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3413 $Mark2Array = $subtable_offset + $this->read_ushort(); // Offset to Mark2Array table
3414 $this->seek($Mark2Coverage);
3415 $Mark2Glyphs = implode('|',$this->_getCoverage());
3416 $checkpos = $ptr;
3417 $checkpos--;
3418 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) {
3419 $checkpos--;
3420 }
3421 if (isset($this->OTLdata[$checkpos]) && strpos($Mark2Glyphs, $this->OTLdata[$checkpos]['hex'])!==false) {
3422 $matchedpos = $checkpos;
3423 }
3424 else { $matchedpos = false; }
3425
3426 if ($matchedpos !== false) {
3427
3428 // Get the relevant MarkRecord
3429 $Mark1Pos = $LuCoverage[$currGID];
3430 $Mark1Record = $this->_getMarkRecord($Mark1Array, $Mark1Pos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3431 //Mark Class is = $Mark1Record['Class']
3432
3433 // Get the relevant Mark2Record
3434 $this->seek($Mark2Array);
3435 $Mark2Count = $this->read_ushort();
3436 $Mark2Pos = strpos($Mark2Glyphs, $this->OTLdata[$matchedpos]['hex'])/6;
3437
3438 // Move to the Mark2Record we want
3439 $nSkip = (2 * $Mark2Pos * $ClassCount );
3440 $this->skip($nSkip);
3441
3442 // Read Mark2Record we want for appropriate Class
3443 $nSkip = 2*$Mark1Record['Class'];
3444 $this->skip($nSkip);
3445 $Mark2RecordOffset = $Mark2Array + $this->read_ushort();
3446 list($x,$y) = $this->_getAnchorTable($Mark2RecordOffset);
3447 $Mark2Record = array('AnchorX'=>$x, 'AnchorY'=>$y); // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3448
3449 // Need default XAdvance for Mark2 glyph
3450 $Mark2Width = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3451
3452
3453 // IF combining marks are set on different components of a ligature glyph, do not apply this rule
3454 // Test: arabictypesetting: &#x625;&#x650;&#x644;&#x64e;&#x649;&#x670;&#x653;
3455 // Test: arabictypesetting: &#x628;&#x651;&#x64e;&#x64a;&#x652;&#x646;&#x64e;&#x643;&#x64f;&#x645;&#x652;
3456 $prevLig = -1;
3457 $thisLig = -1;
3458 $prevComp = -1;
3459 $thisComp = -1;
3460 if (isset($this->assocMarks[$matchedpos])) {
3461 $prevLig = $this->assocMarks[$matchedpos]['ligPos'];
3462 $prevComp = $this->assocMarks[$matchedpos]['compID'];
3463 }
3464 if (isset($this->assocMarks[$ptr])) {
3465 $thisLig = $this->assocMarks[$ptr]['ligPos'];
3466 $thisComp = $this->assocMarks[$ptr]['compID'];
3467 }
3468
3469 // However IF Mark2 (first in logical order, i.e. being attached to) is not associated with a base, carry on
3470 // This happens in Indic when the Mark being attached to e.g. [Halant Ma lig] -> MatraU, [U+0B4D + U+B2E as E0F5]-> U+0B41 become E135
3471 if (!defined("OMIT_OTL_FIX_1") || OMIT_OTL_FIX_1 != 1) {
3472 /* OTL_FIX_1 */
3473 if (isset($this->assocMarks[$matchedpos]) && ($prevLig != $thisLig || $prevComp != $thisComp )) { return 0; }
3474 }
3475 else {
3476 /* Original code */
3477 if ($prevLig != $thisLig || $prevComp != $thisComp ) { return 0; }
3478 }
3479
3480
3481 if (!defined("OMIT_OTL_FIX_2") || OMIT_OTL_FIX_2 != 1) {
3482 /* OTL_FIX_2 */
3483 if (!isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) || !$this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) { $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $Mark2Width; }
3484 }
3485
3486 // ZZZ99Q - Test Case font-family: garuda &#xe19;&#xe49;&#xe33;
3487 if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) && $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) { $this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']; }
3488
3489 // Align to previous Mark by attachment - so need to add the previous placement values
3490 $prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3491 $prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3492 $this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $Mark2Record['AnchorX'] - $Mark1Record['AnchorX'];
3493 $this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $Mark2Record['AnchorY'] - $Mark1Record['AnchorY'];
3494 if ($this->debugOTL) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3495 return 1;
3496
3497 }
3498 return 0;
3499 }
3500
3501 ////////////////////////////////////////////////////////////////////////////////
3502 // LookupType 7: Context positioning Position one or more glyphs in context
3503 ////////////////////////////////////////////////////////////////////////////////
3504 else if ($Type == 7) {
3505 //===========
3506 // Format 1:
3507 //===========
3508 if ($PosFormat==1) {
3509 die("GPOS Lookup Type ".$Type." Format ".$PosFormat." not TESTED YET.");
3510 return 0;
3511 }
3512 //===========
3513 // Format 2:
3514 //===========
3515 else if ($PosFormat==2) {
3516 $CoverageTableOffset = $subtable_offset + $this->read_ushort();
3517 $InputClassDefOffset = $subtable_offset + $this->read_ushort();
3518 $PosClassSetCnt = $this->read_ushort();
3519 $PosClassSetOffset = array();
3520 for ($b=0;$b<$PosClassSetCnt;$b++) {
3521 $offset = $this->read_ushort();
3522 if ($offset==0x0000) {
3523 $PosClassSetOffset[] = $offset;
3524 }
3525 else {
3526 $PosClassSetOffset[] = $subtable_offset + $offset;
3527 }
3528 }
3529
3530 $InputClasses = $this->_getClasses($InputClassDefOffset);
3531
3532 for ($s=0;$s<$PosClassSetCnt;$s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3533 // Select $PosClassSet if currGlyph is in First Input Class
3534 if ($PosClassSetOffset[$s]>0 && isset($InputClasses[$s][$currGID])) {
3535 $this->seek($PosClassSetOffset[$s]);
3536 $PosClassRuleCnt = $this->read_ushort();
3537 $PosClassRule = array();
3538 for($b=0;$b<$PosClassRuleCnt;$b++) {
3539 $PosClassRule[$b] = $PosClassSetOffset[$s]+$this->read_ushort();
3540 }
3541
3542 for($b=0;$b<$PosClassRuleCnt;$b++) { // EACH RULE
3543 $this->seek($PosClassRule[$b]);
3544 $InputGlyphCount = $this->read_ushort();
3545 $PosCount = $this->read_ushort();
3546
3547 $Input = array();
3548 for ($r=1;$r<$InputGlyphCount;$r++) {
3549 $Input[$r] = $this->read_ushort();
3550 }
3551 $inputClass = $s;
3552
3553 $inputGlyphs = array();
3554 $inputGlyphs[0] = $InputClasses[$inputClass];
3555
3556 if ($InputGlyphCount>1) {
3557 // NB starts at 1
3558 for ($gcl=1;$gcl<$InputGlyphCount;$gcl++) {
3559 $classindex = $Input[$gcl];
3560 if (isset($InputClasses[$classindex])) { $inputGlyphs[$gcl] = $InputClasses[$classindex]; }
3561 else { $inputGlyphs[$gcl] = ''; }
3562 }
3563 }
3564
3565 // Class 0 contains all the glyphs NOT in the other classes
3566 $class0excl = array();
3567 for ($gc=1;$gc<=count($InputClasses);$gc++) {
3568 if (is_array($InputClasses[$gc])) $class0excl = $class0excl + $InputClasses[$gc];
3569 }
3570
3571 $backtrackGlyphs = array();
3572 $lookaheadGlyphs = array();
3573
3574 $matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl );
3575 if ($matched) {
3576 for ($p=0;$p<$PosCount;$p++) { // EACH LOOKUP
3577 $SequenceIndex[$p] = $this->read_ushort();
3578 $LookupListIndex[$p] = $this->read_ushort();
3579 }
3580
3581 for ($p=0;$p<$PosCount;$p++) {
3582 // Apply $LookupListIndex at $SequenceIndex
3583 if ($SequenceIndex[$p] >= $InputGlyphCount) { continue; }
3584 $lu = $LookupListIndex[$p];
3585 $luType = $this->GPOSLookups[$lu]['Type'];
3586 $luFlag = $this->GPOSLookups[$lu]['Flag'];
3587 $luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3588
3589 $luptr = $matched[$SequenceIndex[$p]];
3590 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
3591 $lucurrGID = $this->OTLdata[$luptr]['uni'];
3592
3593 foreach($this->GPOSLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
3594 $shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
3595 if ($this->debugOTL && $shift) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3596 if ($shift) { break; }
3597 }
3598 }
3599
3600 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
3601 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
3602
3603 }
3604 }
3605
3606 }
3607 }
3608
3609 return 0;
3610 }
3611 //===========
3612 // Format 3:
3613 //===========
3614 else if ($PosFormat==3) {
3615 die("GPOS Lookup Type ".$Type." Format ".$PosFormat." not TESTED YET.");
3616 return 0;
3617 }
3618 else { die("GPOS Lookup Type ".$Type.", Format ".$PosFormat." not supported."); }
3619 }
3620
3621 ////////////////////////////////////////////////////////////////////////////////
3622 // LookupType 8: Chained Context positioning Position one or more glyphs in chained context
3623 ////////////////////////////////////////////////////////////////////////////////
3624 else if ($Type == 8) {
3625 //===========
3626 // Format 1:
3627 //===========
3628 if ($PosFormat==1) {
3629 die("GPOS Lookup Type ".$Type." Format ".$PosFormat." not TESTED YET.");
3630 return 0;
3631 }
3632 //===========
3633 // Format 2:
3634 //===========
3635 else if ($PosFormat==2) {
3636
3637 $CoverageTableOffset = $subtable_offset + $this->read_ushort();
3638 $BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
3639 $InputClassDefOffset = $subtable_offset + $this->read_ushort();
3640 $LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
3641 $ChainPosClassSetCnt = $this->read_ushort();
3642 $ChainPosClassSetOffset = array();
3643 for ($b=0;$b<$ChainPosClassSetCnt;$b++) {
3644 $offset = $this->read_ushort();
3645 if ($offset==0x0000) {
3646 $ChainPosClassSetOffset[] = $offset;
3647 }
3648 else {
3649 $ChainPosClassSetOffset[] = $subtable_offset + $offset;
3650 }
3651 }
3652
3653 $BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
3654 $InputClasses = $this->_getClasses($InputClassDefOffset);
3655 $LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
3656
3657 for ($s=0;$s<$ChainPosClassSetCnt;$s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3658 // Select $ChainPosClassSet if currGlyph is in First Input Class
3659 if ($ChainPosClassSetOffset[$s]>0 && isset($InputClasses[$s][$currGID])) {
3660 $this->seek($ChainPosClassSetOffset[$s]);
3661 $ChainPosClassRuleCnt = $this->read_ushort();
3662 $ChainPosClassRule = array();
3663 for($b=0;$b<$ChainPosClassRuleCnt;$b++) {
3664 $ChainPosClassRule[$b] = $ChainPosClassSetOffset[$s]+$this->read_ushort();
3665 }
3666
3667 for($b=0;$b<$ChainPosClassRuleCnt;$b++) { // EACH RULE
3668 $this->seek($ChainPosClassRule[$b]);
3669 $BacktrackGlyphCount = $this->read_ushort();
3670 $Backtrack = array();
3671 for ($r=0;$r<$BacktrackGlyphCount;$r++) {
3672 $Backtrack[$r] = $this->read_ushort();
3673 }
3674 $InputGlyphCount = $this->read_ushort();
3675 $Input = array();
3676 for ($r=1;$r<$InputGlyphCount;$r++) {
3677 $Input[$r] = $this->read_ushort();
3678 }
3679 $LookaheadGlyphCount = $this->read_ushort();
3680 $Lookahead = array();
3681 for ($r=0;$r<$LookaheadGlyphCount;$r++) {
3682 $Lookahead[$r] = $this->read_ushort();
3683 }
3684
3685 $inputClass = $s; //???
3686
3687 $inputGlyphs = array();
3688 $inputGlyphs[0] = $InputClasses[$inputClass];
3689
3690 if ($InputGlyphCount>1) {
3691 // NB starts at 1
3692 for ($gcl=1;$gcl<$InputGlyphCount;$gcl++) {
3693 $classindex = $Input[$gcl];
3694 if (isset($InputClasses[$classindex])) { $inputGlyphs[$gcl] = $InputClasses[$classindex]; }
3695 else { $inputGlyphs[$gcl] = ''; }
3696 }
3697 }
3698
3699 // Class 0 contains all the glyphs NOT in the other classes
3700 $class0excl = array();
3701 for ($gc=1;$gc<=count($InputClasses);$gc++) {
3702 if (isset($InputClasses[$gc]) && is_array($InputClasses[$gc])) $class0excl = $class0excl + $InputClasses[$gc];
3703 }
3704
3705 if ($BacktrackGlyphCount) {
3706 $backtrackGlyphs = array();
3707 for ($gcl=0;$gcl<$BacktrackGlyphCount;$gcl++) {
3708 $classindex = $Backtrack[$gcl];
3709 if (isset($BacktrackClasses[$classindex])) { $backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex]; }
3710 else { $backtrackGlyphs[$gcl] = ''; }
3711 }
3712 }
3713 else { $backtrackGlyphs = array(); }
3714
3715 // Class 0 contains all the glyphs NOT in the other classes
3716 $bclass0excl = array();
3717 for ($gc=1;$gc<=count($BacktrackClasses);$gc++) {
3718 if (isset($BacktrackClasses[$gc]) && is_array($BacktrackClasses[$gc])) $bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
3719 }
3720
3721 if ($LookaheadGlyphCount) {
3722 $lookaheadGlyphs = array();
3723 for ($gcl=0;$gcl<$LookaheadGlyphCount;$gcl++) {
3724 $classindex = $Lookahead[$gcl];
3725 if (isset($LookaheadClasses[$classindex])) { $lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex]; }
3726 else { $lookaheadGlyphs[$gcl] = ''; }
3727 }
3728 }
3729 else { $lookaheadGlyphs = array(); }
3730
3731 // Class 0 contains all the glyphs NOT in the other classes
3732 $lclass0excl = array();
3733 for ($gc=1;$gc<=count($LookaheadClasses);$gc++) {
3734 if (isset($LookaheadClasses[$gc]) && is_array($LookaheadClasses[$gc])) $lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
3735 }
3736
3737 $matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl );
3738 if ($matched) {
3739 $PosCount = $this->read_ushort();
3740 $SequenceIndex = array();
3741 $LookupListIndex = array();
3742 for ($p=0;$p<$PosCount;$p++) { // EACH LOOKUP
3743 $SequenceIndex[$p] = $this->read_ushort();
3744 $LookupListIndex[$p] = $this->read_ushort();
3745 }
3746
3747 for ($p=0;$p<$PosCount;$p++) {
3748 // Apply $LookupListIndex at $SequenceIndex
3749 if ($SequenceIndex[$p] >= $InputGlyphCount) { continue; }
3750 $lu = $LookupListIndex[$p];
3751 $luType = $this->GPOSLookups[$lu]['Type'];
3752 $luFlag = $this->GPOSLookups[$lu]['Flag'];
3753 $luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3754
3755 $luptr = $matched[$SequenceIndex[$p]];
3756 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
3757 $lucurrGID = $this->OTLdata[$luptr]['uni'];
3758
3759 foreach($this->GPOSLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
3760 $shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
3761 if ($this->debugOTL && $shift) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3762 if ($shift) { break; }
3763 }
3764 }
3765
3766 if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) { return $shift ; } /* OTL_FIX_3 */
3767 else return $InputGlyphCount ; // should be + matched ignores in Input Sequence
3768
3769 }
3770 }
3771
3772 }
3773 }
3774
3775 return 0;
3776 }
3777 //===========
3778 // Format 3:
3779 //===========
3780 else if ($PosFormat==3) {
3781 $BacktrackGlyphCount = $this->read_ushort();
3782 for ($b=0;$b<$BacktrackGlyphCount;$b++) {
3783 $CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3784 }
3785 $InputGlyphCount = $this->read_ushort();
3786 for ($b=0;$b<$InputGlyphCount;$b++) {
3787 $CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3788 }
3789 $LookaheadGlyphCount = $this->read_ushort();
3790 for ($b=0;$b<$LookaheadGlyphCount;$b++) {
3791 $CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3792 }
3793 $PosCount = $this->read_ushort();
3794 $save_pos = $this->_pos; // Save the point just after PosCount
3795
3796 $CoverageBacktrackGlyphs = array();
3797 for ($b=0;$b<$BacktrackGlyphCount;$b++) {
3798 $this->seek($CoverageBacktrackOffset[$b]);
3799 $glyphs = $this->_getCoverage();
3800 $CoverageBacktrackGlyphs[$b] = implode("|",$glyphs);
3801 }
3802 $CoverageInputGlyphs = array();
3803 for ($b=0;$b<$InputGlyphCount;$b++) {
3804 $this->seek($CoverageInputOffset[$b]);
3805 $glyphs = $this->_getCoverage();
3806 $CoverageInputGlyphs[$b] = implode("|",$glyphs);
3807 }
3808 $CoverageLookaheadGlyphs = array();
3809 for ($b=0;$b<$LookaheadGlyphCount;$b++) {
3810 $this->seek($CoverageLookaheadOffset[$b]);
3811 $glyphs = $this->_getCoverage();
3812 $CoverageLookaheadGlyphs[$b] = implode("|",$glyphs);
3813 }
3814 $matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs , $ignore, $ptr);
3815 if ($matched) {
3816
3817 $this->seek($save_pos); // Return to just after PosCount
3818 for ($p=0;$p<$PosCount;$p++) {
3819 // PosLookupRecord
3820 $PosLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
3821 $PosLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
3822 }
3823 for ($p=0;$p<$PosCount;$p++) {
3824 // Apply $PosLookupRecord[$p]['LookupListIndex'] at $PosLookupRecord[$p]['SequenceIndex']
3825 if ($PosLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) { continue; }
3826 $lu = $PosLookupRecord[$p]['LookupListIndex'];
3827 $luType = $this->GPOSLookups[$lu]['Type'];
3828 $luFlag = $this->GPOSLookups[$lu]['Flag'];
3829 if (isset($this->GPOSLookups[$lu]['MarkFilteringSet'])) { $luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet']; }
3830 else { $luMarkFilteringSet = ''; }
3831
3832 $luptr = $matched[$PosLookupRecord[$p]['SequenceIndex']];
3833 $lucurrGlyph = $this->OTLdata[$luptr]['hex'];
3834 $lucurrGID = $this->OTLdata[$luptr]['uni'];
3835
3836 foreach($this->GPOSLookups[$lu]['Subtables'] AS $luc=>$lusubtable_offset) {
3837 $shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
3838 if ($this->debugOTL && $shift) { $this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level); }
3839 if ($shift) { break; }
3840 }
3841 }
3842 }
3843
3844
3845 }
3846 else { die("GPOS Lookup Type ".$Type.", Format ".$PosFormat." not supported."); }
3847 }
3848
3849 else { die("GPOS Lookup Type ".$Type." not supported."); }
3850 }
3851
3852 //////////////////////////////////////////////////////////////////////////////////
3853 //////////////////////////////////////////////////////////////////////////////////
3854 // GPOS / GSUB / GCOM (common) functions
3855 //////////////////////////////////////////////////////////////////////////////////
3856 //////////////////////////////////////////////////////////////////////////////////
3857
3858 function checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr) {
3859 // Input etc are single numbers - GSUB Format 6.1
3860 // Input starts with (1=>xxx)
3861 // return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
3862
3863 $current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
3864
3865 // BACKTRACK
3866 $checkpos = $ptr;
3867 for ($i=0;$i<count($Backtrack);$i++) {
3868 $checkpos--;
3869 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos--; }
3870 // If outside scope of current syllable - return no match
3871 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3872 return false;
3873 }
3874 else if (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Backtrack[$i]) {
3875 return false;
3876 }
3877 }
3878
3879 // INPUT
3880 $matched = array(0=>$ptr);
3881 $checkpos = $ptr;
3882 for ($i=1;$i<count($Input);$i++) {
3883 $checkpos++;
3884 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
3885 // If outside scope of current syllable - return no match
3886 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3887 return false;
3888 }
3889 else if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == $Input[$i]) {
3890 $matched[] = $checkpos;
3891 }
3892 else { return false; }
3893 }
3894
3895 // LOOKAHEAD
3896 for ($i=0;$i<count($Lookahead);$i++) {
3897 $checkpos++;
3898 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
3899 // If outside scope of current syllable - return no match
3900 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3901 return false;
3902 }
3903 else if (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Lookahead[$i]) {
3904 return false;
3905 }
3906 }
3907
3908 return $matched;
3909 }
3910
3911
3912 function checkContextMatchMultiple($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl='', $bclass0excl='', $lclass0excl='') {
3913 // Input etc are string/array of glyph strings - GSUB Format 5.2, 5.3, 6.2, 6.3, GPOS Format 7.2, 7.3, 8.2, 8.3
3914 // Input starts with (1=>xxx)
3915 // return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
3916 // $class0excl is the string of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
3917 // $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
3918
3919 $current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
3920
3921 // BACKTRACK
3922 $checkpos = $ptr;
3923 for ($i=0;$i<count($Backtrack);$i++) {
3924 $checkpos--;
3925 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos--; }
3926 // If outside scope of current syllable - return no match
3927 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3928 return false;
3929 }
3930 // If Class 0 specified, matches anything NOT in $bclass0excl
3931 else if (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && strpos($bclass0excl,$this->OTLdata[$checkpos]['hex'])!==false) {
3932 return false;
3933 }
3934 else if (!isset($this->OTLdata[$checkpos]) || strpos($Backtrack[$i], $this->OTLdata[$checkpos]['hex'])===false) {
3935 return false;
3936 }
3937 }
3938
3939 // INPUT
3940 $matched = array(0=>$ptr);
3941 $checkpos = $ptr;
3942 for ($i=1;$i<count($Input);$i++) { // Start at 1 - already matched the first InputGlyph
3943 $checkpos++;
3944 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
3945 // If outside scope of current syllable - return no match
3946 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3947 return false;
3948 }
3949 // If Input Class 0 specified, matches anything NOT in $class0excl
3950 else if (!$Input[$i] && isset($this->OTLdata[$checkpos]) && strpos($class0excl,$this->OTLdata[$checkpos]['hex'])===false) {
3951 $matched[] = $checkpos;
3952 }
3953 else if (isset($this->OTLdata[$checkpos]) && strpos($Input[$i],$this->OTLdata[$checkpos]['hex'])!==false) {
3954 $matched[] = $checkpos;
3955 }
3956 else { return false; }
3957 }
3958
3959 // LOOKAHEAD
3960 for ($i=0;$i<count($Lookahead);$i++) {
3961 $checkpos++;
3962 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
3963 // If outside scope of current syllable - return no match
3964 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3965 return false;
3966 }
3967 // If Class 0 specified, matches anything NOT in $lclass0excl
3968 else if (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && strpos($lclass0excl,$this->OTLdata[$checkpos]['hex'])!==false) {
3969 return false;
3970 }
3971 else if (!isset($this->OTLdata[$checkpos]) || strpos($Lookahead[$i],$this->OTLdata[$checkpos]['hex'])===false) {
3972 return false;
3973 }
3974 }
3975 return $matched;
3976 }
3977
3978 function checkContextMatchMultipleUni($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl=array(), $bclass0excl=array(), $lclass0excl=array()) {
3979 // Input etc are array of glyphs - GSUB Format 5.2, 5.3, 6.2, 6.3, GPOS Format 7.2, 7.3, 8.2, 8.3
3980 // Input starts with (1=>xxx)
3981 // return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
3982 // $class0excl is array of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
3983 // $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
3984
3985 $current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
3986
3987 // BACKTRACK
3988 $checkpos = $ptr;
3989 for ($i=0;$i<count($Backtrack);$i++) {
3990 $checkpos--;
3991 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos--; }
3992 // If outside scope of current syllable - return no match
3993 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
3994 return false;
3995 }
3996 // If Class 0 specified, matches anything NOT in $bclass0excl
3997 else if (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && isset($bclass0excl[$this->OTLdata[$checkpos]['uni']]) ) {
3998 return false;
3999 }
4000 else if (!isset($this->OTLdata[$checkpos]) || !isset($Backtrack[$i][$this->OTLdata[$checkpos]['uni']])) {
4001 return false;
4002 }
4003 }
4004
4005 // INPUT
4006 $matched = array(0=>$ptr);
4007 $checkpos = $ptr;
4008 for ($i=1;$i<count($Input);$i++) { // Start at 1 - already matched the first InputGlyph
4009 $checkpos++;
4010 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
4011 // If outside scope of current syllable - return no match
4012 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4013 return false;
4014 }
4015 // If Input Class 0 specified, matches anything NOT in $class0excl
4016 else if (!$Input[$i] && isset($this->OTLdata[$checkpos]) && !isset($class0excl[$this->OTLdata[$checkpos]['uni']]) ) {
4017 $matched[] = $checkpos;
4018 }
4019 else if (isset($this->OTLdata[$checkpos]) && isset($Input[$i][$this->OTLdata[$checkpos]['uni']])) {
4020 $matched[] = $checkpos;
4021 }
4022 else { return false; }
4023 }
4024
4025 // LOOKAHEAD
4026 for ($i=0;$i<count($Lookahead);$i++) {
4027 $checkpos++;
4028 while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex'])!==false) { $checkpos++; }
4029 // If outside scope of current syllable - return no match
4030 if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4031 return false;
4032 }
4033 // If Class 0 specified, matches anything NOT in $lclass0excl
4034 else if (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && isset($lclass0excl[$this->OTLdata[$checkpos]['uni']]) ) {
4035 return false;
4036 }
4037 else if (!isset($this->OTLdata[$checkpos]) || !isset($Lookahead[$i][$this->OTLdata[$checkpos]['uni']])) {
4038 return false;
4039 }
4040 }
4041 return $matched;
4042 }
4043
4044
4045
4046
4047
4048 function _getClassDefinitionTable($offset) {
4049 if (isset($this->LuDataCache[$this->fontkey][$offset])) {
4050 $GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
4051 }
4052 else {
4053 $this->seek($offset);
4054 $ClassFormat = $this->read_ushort();
4055 $GlyphClass = array();
4056 // $GlyphByClass = array(0=>array()); // NB This forces an index[0]
4057 if ($ClassFormat == 1) {
4058 $StartGlyph = $this->read_ushort();
4059 $GlyphCount = $this->read_ushort();
4060 for ($i=0;$i<$GlyphCount;$i++) {
4061 $GlyphClass[$i]['startGlyphID'] = $StartGlyph + $i;
4062 $GlyphClass[$i]['endGlyphID'] = $StartGlyph + $i;
4063 $GlyphClass[$i]['class'] = $this->read_ushort();
4064 for($g=$GlyphClass[$i]['startGlyphID'];$g<=$GlyphClass[$i]['endGlyphID'];$g++) {
4065 $GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4066 }
4067 }
4068 }
4069 else if ($ClassFormat == 2) {
4070 $tableCount = $this->read_ushort();
4071 for ($i=0;$i<$tableCount;$i++) {
4072 $GlyphClass[$i]['startGlyphID'] = $this->read_ushort();
4073 $GlyphClass[$i]['endGlyphID'] = $this->read_ushort();
4074 $GlyphClass[$i]['class'] = $this->read_ushort();
4075 for($g=$GlyphClass[$i]['startGlyphID'];$g<=$GlyphClass[$i]['endGlyphID'];$g++) {
4076 $GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4077 }
4078 }
4079 }
4080 ksort($GlyphByClass);
4081 $this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
4082 }
4083 return $GlyphByClass;
4084 }
4085
4086 function count_bits($n) {
4087 for ($c=0; $n; $c++) {
4088 $n &= $n - 1; // clear the least significant bit set
4089 }
4090 return $c;
4091 }
4092
4093 function _getValueRecord($ValueFormat) { // Common ValueRecord for GPOS
4094 // Only returns 3 possible: $vra['XPlacement'] $vra['YPlacement'] $vra['XAdvance']
4095 $vra = array();
4096 // Horizontal adjustment for placement - in design units
4097 if (($ValueFormat & 0x0001) == 0x0001) { $vra['XPlacement'] = $this->read_short(); }
4098 // Vertical adjustment for placement - in design units
4099 if (($ValueFormat & 0x0002) == 0x0002) { $vra['YPlacement'] = $this->read_short(); }
4100 // Horizontal adjustment for advance - in design units (only used for horizontal writing)
4101 if (($ValueFormat & 0x0004) == 0x0004) { $vra['XAdvance'] = $this->read_short(); }
4102 // Vertical adjustment for advance - in design units (only used for vertical writing)
4103 if (($ValueFormat & 0x0008) == 0x0008) { $this->read_short(); }
4104 // Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)
4105 if (($ValueFormat & 0x0010) == 0x0010) { $this->read_ushort(); }
4106 // Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)
4107 if (($ValueFormat & 0x0020) == 0x0020) { $this->read_ushort(); }
4108 // Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)
4109 if (($ValueFormat & 0x0040) == 0x0040) { $this->read_ushort(); }
4110 // Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)
4111 if (($ValueFormat & 0x0080) == 0x0080) { $this->read_ushort(); }
4112 return $vra;
4113 }
4114
4115 function _getAnchorTable($offset=0) {
4116 if ($offset) { $this->seek($offset); }
4117 $AnchorFormat = $this->read_ushort();
4118 $XCoordinate = $this->read_short();
4119 $YCoordinate = $this->read_short();
4120 // Format 2 specifies additional link to contour point; Format 3 additional Device table
4121 return array($XCoordinate, $YCoordinate);
4122 }
4123
4124 function _getMarkRecord($offset, $MarkPos) {
4125 $this->seek($offset);
4126 $MarkCount = $this->read_ushort();
4127 $this->skip($MarkPos*4);
4128 $Class = $this->read_ushort();
4129 $MarkAnchor = $offset + $this->read_ushort(); // = Offset to anchor table
4130 list($x,$y) = $this->_getAnchorTable($MarkAnchor );
4131 $MarkRecord = array('Class'=>$Class, 'AnchorX'=>$x, 'AnchorY'=>$y);
4132 return $MarkRecord;
4133 }
4134
4135 function _getGCOMignoreString($flag, $MarkFilteringSet) {
4136 // If ignoreFlag set, combine all ignore glyphs into -> "(?:( 0FBA1| 0FBA2| 0FBA3)*)"
4137 // else "()"
4138 // for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup
4139 $str = "";
4140 $ignoreflag = 0;
4141
4142 // Flag & 0xFF?? = MarkAttachmentType
4143 if ($flag & 0xFF00) {
4144 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4145 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4146 $MarkAttachmentType = $flag >> 8;
4147 $ignoreflag = $flag;
4148 $str = $this->MarkAttachmentType[$MarkAttachmentType];
4149 }
4150
4151 // Flag & 0x0010 = UseMarkFilteringSet
4152 if ($flag & 0x0010) {
4153 die("This font [".$this->fontkey."] contains MarkGlyphSets - Not tested yet");
4154 // Change also in ttfontsuni.php
4155 if ($MarkFilteringSet=='') die("This font [".$this->fontkey."] contains MarkGlyphSets - but MarkFilteringSet not set");
4156 $str = $this->MarkGlyphSets[$MarkFilteringSet];
4157 }
4158
4159 // If Ignore Marks set, supercedes any above
4160 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4161 if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {
4162 $ignoreflag = 8;
4163 $str = $this->GlyphClassMarks;
4164 }
4165
4166 // Flag & 0x0004 = Ignore Ligatures
4167 if (($flag & 0x0004) == 0x0004) {
4168 $ignoreflag += 4;
4169 if ($str) { $str .= "|"; }
4170 $str .= $this->GlyphClassLigatures;
4171 }
4172 // Flag & 0x0002 = Ignore BaseGlyphs
4173 if (($flag & 0x0002) == 0x0002) {
4174 $ignoreflag += 2;
4175 if ($str) { $str .= "|"; }
4176 $str .= $this->GlyphClassBases;
4177 }
4178 if ($str) { return "((?:(?:" . $str . "))*)"; }
4179 else return "()";
4180 }
4181
4182 function _checkGCOMignore($flag, $glyph, $MarkFilteringSet) {
4183 $ignore = false;
4184 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4185 if (($flag & 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks,$glyph)) { $ignore = true; }
4186 if (($flag & 0x0004) && strpos($this->GlyphClassLigatures,$glyph)) { $ignore = true; }
4187 if (($flag & 0x0002) && strpos($this->GlyphClassBases,$glyph)) { $ignore = true; }
4188 // Flag & 0xFF?? = MarkAttachmentType
4189 if ($flag & 0xFF00) {
4190 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4191 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4192 if (strpos($this->MarkAttachmentType[($flag >> 8)],$glyph)) { $ignore = true; }
4193 }
4194 // Flag & 0x0010 = UseMarkFilteringSet
4195 if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet],$glyph)) { $ignore = true; }
4196 return $ignore;
4197 }
4198
4199 ////////////////////////////////////////////////////////////////
4200 ////////////////////////////////////////////////////////////////
4201 ////////// BIDI ALGORITHM ////////////////////////
4202 ////////////////////////////////////////////////////////////////
4203 ////////////////////////////////////////////////////////////////
4204 ////////////////////////////////////////////////////////////////
4205 ////////////////////////////////////////////////////////////////
4206 // These functions are called from mpdf after GSUB/GPOS has taken place
4207 // At this stage the bidi-type is in string form
4208 ////////////////////////////////////////////////////////////////
4209 ////////////////////////////////////////////////////////////////
4210 /*
4211 Bidirectional Character Types
4212 =============================
4213 Type Description General Scope
4214 Strong
4215 L Left-to-Right LRM, most alphabetic, syllabic, Han ideographs, non-European or non-Arabic digits, ...
4216 LRE Left-to-Right Embedding LRE
4217 LRO Left-to-Right Override LRO
4218 R Right-to-Left RLM, Hebrew alphabet, and related punctuation
4219 AL Right-to-Left Arabic Arabic, Thaana, and Syriac alphabets, most punctuation specific to those scripts, ...
4220 RLE Right-to-Left Embedding RLE
4221 RLO Right-to-Left Override RLO
4222 Weak
4223 PDF Pop Directional Format PDF
4224 EN European Number European digits, Eastern Arabic-Indic digits, ...
4225 ES European Number Separator Plus sign, minus sign
4226 ET European Number Terminator Degree sign, currency symbols, ...
4227 AN Arabic Number Arabic-Indic digits, Arabic decimal and thousands separators, ...
4228 CS Common Number Separator Colon, comma, full stop (period), No-break space, ...
4229 NSM Nonspacing Mark Characters marked Mn (Nonspacing_Mark) and Me (Enclosing_Mark) in the Unicode Character Database
4230 BN Boundary Neutral Default ignorables, non-characters, and control characters, other than those explicitly given other types.
4231 Neutral
4232 B Paragraph Separator Paragraph separator, appropriate Newline Functions, higher-level protocol paragraph determination
4233 S Segment Separator Tab
4234 WS Whitespace Space, figure space, line separator, form feed, General Punctuation spaces, ...
4235 ON Other Neutrals All other characters, including OBJECT REPLACEMENT CHARACTER
4236 */
4237
4238 function _bidiSort($ta, $str='', $dir, &$chunkOTLdata, $useGPOS) {
4239
4240 $pel = 0; // paragraph embedding level
4241 $maxlevel = 0;
4242 $numchars = count($chunkOTLdata['char_data']);
4243
4244 // Set the initial paragraph embedding level
4245 if ($dir == 'rtl') { $pel = 1; }
4246 else { $pel = 0; }
4247
4248
4249 // X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4250 // Current Embedding Level
4251 $cel = $pel;
4252 // directional override status (-1 is Neutral)
4253 $dos = -1;
4254 $remember = array();
4255
4256 // Array of characters data
4257 $chardata = Array();
4258
4259 // Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
4260 // In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
4261 for ($i=0; $i < $numchars; ++$i) {
4262 if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
4263 // X2. With each RLE, compute the least greater odd embedding level.
4264 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4265 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4266 $next_level = $cel + ($cel % 2) + 1;
4267 if ($next_level < 62) {
4268 $remember[] = array('num' => 8235, 'cel' => $cel, 'dos' => $dos);
4269 $cel = $next_level;
4270 $dos = -1;
4271 }
4272 }
4273 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
4274 // X3. With each LRE, compute the least greater even embedding level.
4275 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4276 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4277 $next_level = $cel + 2 - ($cel % 2);
4278 if ( $next_level < 62 ) {
4279 $remember[] = array('num' => 8234, 'cel' => $cel, 'dos' => $dos);
4280 $cel = $next_level;
4281 $dos = -1;
4282 }
4283 }
4284 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
4285 // X4. With each RLO, compute the least greater odd embedding level.
4286 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to right-to-left.
4287 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4288 $next_level = $cel + ($cel % 2) + 1;
4289 if ($next_level < 62) {
4290 $remember[] = array('num' => 8238, 'cel' => $cel, 'dos' => $dos);
4291 $cel = $next_level;
4292 $dos = UCDN::BIDI_CLASS_R;
4293 }
4294 }
4295 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
4296 // X5. With each LRO, compute the least greater even embedding level.
4297 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to left-to-right.
4298 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4299 $next_level = $cel + 2 - ($cel % 2);
4300 if ( $next_level < 62 ) {
4301 $remember[] = array('num' => 8237, 'cel' => $cel, 'dos' => $dos);
4302 $cel = $next_level;
4303 $dos = UCDN::BIDI_CLASS_L;
4304 }
4305 }
4306 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
4307 // X7. With each PDF, determine the matching embedding or override code. If there was a valid matching code, restore (pop) the last remembered (pushed) embedding level and directional override.
4308 if (count($remember)) {
4309 $last = count($remember ) - 1;
4310 if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
4311 ($remember[$last]['num'] == 8237)) {
4312 $match = array_pop($remember);
4313 $cel = $match['cel'];
4314 $dos = $match['dos'];
4315 }
4316 }
4317 }
4318 else if ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
4319 // Reset to start values
4320 $cel = $pel;
4321 $dos = -1;
4322 $remember = array();
4323 }
4324 else {
4325 // X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
4326 // a. Set the level of the current character to the current embedding level.
4327 // b. When the directional override status is not neutral, reset the current character type to directional override status.
4328 if ($dos != -1) { $chardir = $dos; }
4329 else {
4330 $chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
4331 }
4332 // stores string characters and other information
4333 if (isset($chunkOTLdata['GPOSinfo'][$i])) { $gpos = $chunkOTLdata['GPOSinfo'][$i]; }
4334 else $gpos = '';
4335 $chardata[] = array('char' => $chunkOTLdata['char_data'][$i]['uni'], 'level' => $cel, 'type' => $chardir, 'group' => $chunkOTLdata['group']{$i}, 'GPOSinfo' => $gpos);
4336 }
4337 }
4338
4339 $numchars = count($chardata);
4340
4341 // X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
4342 // Paragraph separators are not included in the embedding.
4343 // X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
4344 // This is effectively done by only saving other codes to chardata
4345
4346 // X10. Determine the start-of-sequence (sor) and end-of-sequence (eor) types, either L or R, for each isolating run sequence. These depend on the higher of the two levels on either side of the sequence boundary:
4347 // For sor, compare the level of the first character in the sequence with the level of the character preceding it in the paragraph or if there is none, with the paragraph embedding level.
4348 // For eor, compare the level of the last character in the sequence with the level of the character following it in the paragraph or if there is none, with the paragraph embedding level.
4349 // If the higher level is odd, the sor or eor is R; otherwise, it is L.
4350
4351 $prelevel = $pel;
4352 $postlevel = $pel;
4353 $cel = $prelevel; // current embedding level
4354 for ($i=0; $i < $numchars; ++$i) {
4355 $level = $chardata[$i]['level'];
4356 if ($i==0) { $left = $prelevel; }
4357 else { $left = $chardata[$i-1]['level']; }
4358 if ($i==($numchars-1)) { $right = $postlevel; }
4359 else { $right = $chardata[$i+1]['level']; }
4360 $chardata[$i]['sor'] = max($left, $level) % 2 ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
4361 $chardata[$i]['eor'] = max($right, $level) % 2 ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
4362 }
4363
4364
4365
4366 // 3.3.3 Resolving Weak Types
4367 // Weak types are now resolved one level run at a time. At level run boundaries where the type of the character on the other side of the boundary is required, the type assigned to sor or eor is used.
4368 // Nonspacing marks are now resolved based on the previous characters.
4369
4370 // W1. Examine each nonspacing mark (NSM) in the level run, and change the type of the NSM to the type of the previous character. If the NSM is at the start of the level run, it will get the type of sor.
4371 for ($i=0; $i < $numchars; ++$i) {
4372 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_NSM) {
4373 if ($i==0 || $chardata[$i]['level']!=$chardata[$i-1]['level']) {
4374 $chardata[$i]['type'] = $chardata[$i]['sor'];
4375 }
4376 else {
4377 $chardata[$i]['type'] = $chardata[($i-1)]['type'];
4378 }
4379 }
4380 }
4381
4382 // W2. Search backward from each instance of a European number until the first strong type (R, L, AL, or sor) is found. If an AL is found, change the type of the European number to Arabic number.
4383 $prevlevel = -1;
4384 $levcount = 0;
4385 for ($i=0; $i < $numchars; ++$i) {
4386 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN) {
4387 $found = false;
4388 for ($j=$levcount; $j >= 0; $j--) {
4389 if ($chardata[$j]['type'] == UCDN::BIDI_CLASS_AL) { $chardata[$i]['type'] = UCDN::BIDI_CLASS_AN; $found = true; break; }
4390 else if (($chardata[$j]['type'] == UCDN::BIDI_CLASS_L) || ($chardata[$j]['type'] == UCDN::BIDI_CLASS_R)) { $found = true; break; }
4391 }
4392 }
4393 if ($chardata[$i]['level'] != $prevlevel) { $levcount = 0; }
4394 else { ++$levcount; }
4395 $prevlevel = $chardata[$i]['level'];
4396 }
4397
4398 // W3. Change all ALs to R.
4399 for ($i=0; $i < $numchars; ++$i) {
4400 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_AL) { $chardata[$i]['type'] = UCDN::BIDI_CLASS_R; }
4401 }
4402
4403 // W4. A single European separator between two European numbers changes to a European number. A single common separator between two numbers of the same type changes to that type.
4404 for ($i=1; $i < $numchars; ++$i) {
4405 if ( ($i+1) < $numchars && $chardata[($i)]['level'] == $chardata[($i+1)]['level'] && $chardata[($i)]['level'] == $chardata[($i-1)]['level']) {
4406 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ES && $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_EN && $chardata[($i+1)]['type'] == UCDN::BIDI_CLASS_EN) {
4407 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4408 }
4409 else if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS && $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_EN && $chardata[($i+1)]['type'] == UCDN::BIDI_CLASS_EN) {
4410 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4411 }
4412 else if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS && $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_AN && $chardata[($i+1)]['type'] == UCDN::BIDI_CLASS_AN) {
4413 $chardata[$i]['type'] = UCDN::BIDI_CLASS_AN;
4414 }
4415 }
4416 }
4417
4418 // W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
4419 for ($i=0; $i < $numchars; ++$i) {
4420 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ET) {
4421 if ($i > 0 && $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_EN && $chardata[($i)]['level'] == $chardata[($i-1)]['level']) {
4422 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4423 }
4424 else {
4425 $j = $i+1;
4426 while ($j < $numchars && $chardata[$j]['level'] == $chardata[$i]['level'] ) {
4427 if ($chardata[$j]['type'] == UCDN::BIDI_CLASS_EN) {
4428 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4429 break;
4430 }
4431 else if ($chardata[$j]['type'] != UCDN::BIDI_CLASS_ET) { break; }
4432 ++$j;
4433 }
4434 }
4435 }
4436 }
4437
4438 // W6. Otherwise, separators and terminators change to Other Neutral.
4439 for ($i=0; $i < $numchars; ++$i) {
4440 if (($chardata[$i]['type'] == UCDN::BIDI_CLASS_ET) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ES) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS)) {
4441 $chardata[$i]['type'] = UCDN::BIDI_CLASS_ON;
4442 }
4443 }
4444
4445 //W7. Search backward from each instance of a European number until the first strong type (R, L, or sor) is found. If an L is found, then change the type of the European number to L.
4446 for ($i=0; $i < $numchars; ++$i) {
4447 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN) {
4448 if ($i==0) { // Start of Level run
4449 if ($chardata[$i]['sor']==UCDN::BIDI_CLASS_L) $chardata[$i]['type'] = $chardata[$i]['sor'];
4450 }
4451 else {
4452 for ($j=$i-1; $j >= 0; $j--) {
4453 if ($chardata[$j]['level'] != $chardata[$i]['level']) { // Level run boundary
4454 if ($chardata[$j+1]['sor']==UCDN::BIDI_CLASS_L) $chardata[$i]['type'] = $chardata[$j+1]['sor'];
4455 break;
4456 }
4457 else if ($chardata[$j]['type'] == UCDN::BIDI_CLASS_L) {
4458 $chardata[$i]['type'] = UCDN::BIDI_CLASS_L;
4459 break;
4460 }
4461 else if ($chardata[$j]['type'] == UCDN::BIDI_CLASS_R) {
4462 break;
4463 }
4464 }
4465 }
4466 }
4467 }
4468
4469 // N1. A sequence of neutrals takes the direction of the surrounding strong text if the text on both sides has the same direction. European and Arabic numbers act as if they were R in terms of their influence on neutrals. Start-of-level-run (sor) and end-of-level-run (eor) are used at level run boundaries.
4470 for ($i=0; $i < $numchars; ++$i) {
4471 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ON || $chardata[$i]['type'] == UCDN::BIDI_CLASS_WS) {
4472 $left = -1;
4473 // LEFT
4474 if ($i==0) { // first char
4475 $left = $chardata[($i)]['sor'];
4476 }
4477 else if ($chardata[($i-1)]['level'] != $chardata[($i)]['level']) { // run boundary
4478 $left = $chardata[($i)]['sor'];
4479 }
4480 else if ($chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_L) {
4481 $left = UCDN::BIDI_CLASS_L;
4482 }
4483 else if ($chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_R || $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_EN || $chardata[($i-1)]['type'] == UCDN::BIDI_CLASS_AN) {
4484 $left = UCDN::BIDI_CLASS_R;
4485 }
4486 // RIGHT
4487 $right = -1;
4488 $j=$i;
4489 // move to the right of any following neutrals OR hit a run boundary
4490 while(($chardata[$j]['type'] == UCDN::BIDI_CLASS_ON || $chardata[$j]['type'] == UCDN::BIDI_CLASS_WS) && $j<=($numchars-1)) {
4491 if ($j==($numchars-1)) { // last char
4492 $right = $chardata[($j)]['eor'];
4493 break;
4494 }
4495 else if ($chardata[($j+1)]['level'] != $chardata[($j)]['level']) { // run boundary
4496 $right = $chardata[($j)]['eor'];
4497 break;
4498 }
4499 else if ($chardata[($j+1)]['type'] == UCDN::BIDI_CLASS_L) {
4500 $right = UCDN::BIDI_CLASS_L;
4501 break;
4502 }
4503 else if ($chardata[($j+1)]['type'] == UCDN::BIDI_CLASS_R || $chardata[($j+1)]['type'] == UCDN::BIDI_CLASS_EN || $chardata[($j+1)]['type'] == UCDN::BIDI_CLASS_AN) {
4504 $right = UCDN::BIDI_CLASS_R;
4505 break;
4506 }
4507 $j++;
4508 }
4509 if ($left > -1 && $left==$right) {
4510 $chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4511 $chardata[$i]['type'] = $left;
4512 }
4513 }
4514 }
4515
4516 // N2. Any remaining neutrals take the embedding direction
4517 for ($i=0; $i < $numchars; ++$i) {
4518 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ON || $chardata[$i]['type'] == UCDN::BIDI_CLASS_WS) {
4519 $chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
4520 $chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4521 }
4522 }
4523
4524 // I1. For all characters with an even (left-to-right) embedding direction, those of type R go up one level and those of type AN or EN go up two levels.
4525 // I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
4526 for ($i=0; $i < $numchars; ++$i) {
4527 $odd = $chardata[$i]['level'] % 2;
4528 if ($odd) {
4529 if (($chardata[$i]['type'] == UCDN::BIDI_CLASS_L) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_AN) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN)) {
4530 $chardata[$i]['level'] += 1;
4531 }
4532 }
4533 else {
4534 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_R) { $chardata[$i]['level'] += 1; }
4535 else if (($chardata[$i]['type'] == UCDN::BIDI_CLASS_AN) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN)) { $chardata[$i]['level'] += 2; }
4536 }
4537 $maxlevel = max($chardata[$i]['level'],$maxlevel);
4538 }
4539
4540 // NB
4541 // Separate into lines at this point************
4542 //
4543
4544 // L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
4545 // 1. Segment separators (Tab) 'S',
4546 // 2. Paragraph separators 'B',
4547 // 3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
4548 // 4. Any sequence of whitespace characters 'WS' at the end of the line.
4549 // The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
4550 // Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
4551
4552 for ($i=($numchars-1); $i>0; $i--) {
4553 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_WS || (isset($chardata[$i]['orig_type']) && $chardata[$i]['orig_type'] == UCDN::BIDI_CLASS_WS)) {
4554 $chardata[$i]['level'] = $pel;
4555 }
4556 else { break; }
4557 }
4558
4559
4560 // L2. From the highest level found in the text to the lowest odd level on each line, including intermediate levels not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
4561 for ($j=$maxlevel; $j > 0; $j--) {
4562 $ordarray = array();
4563 $revarr = array();
4564 $onlevel = false;
4565 for ($i=0; $i < $numchars; ++$i) {
4566 if ($chardata[$i]['level'] >= $j) {
4567 $onlevel = true;
4568
4569 // L4. A character is depicted by a mirrored glyph if and only if (a) the resolved directionality of that character is R, and (b) the Bidi_Mirrored property value of that character is true.
4570 if (isset(UCDN::$mirror_pairs[$chardata[$i]['char']]) && $chardata[$i]['type']==UCDN::BIDI_CLASS_R) {
4571 $chardata[$i]['char'] = UCDN::$mirror_pairs[$chardata[$i]['char']];
4572 }
4573
4574 $revarr[] = $chardata[$i];
4575 }
4576 else {
4577 if ($onlevel) {
4578 $revarr = array_reverse($revarr);
4579 $ordarray = array_merge($ordarray, $revarr);
4580 $revarr = Array();
4581 $onlevel = false;
4582 }
4583 $ordarray[] = $chardata[$i];
4584 }
4585 }
4586 if ($onlevel) {
4587 $revarr = array_reverse($revarr);
4588 $ordarray = array_merge($ordarray, $revarr);
4589 }
4590 $chardata = $ordarray;
4591 }
4592
4593 $group = '';
4594 $e = '';
4595 $GPOS = array();
4596 $cctr = 0;
4597 $rtl_content = 0x0;
4598 foreach ($chardata as $cd) {
4599 $e.=code2utf($cd['char']);
4600 $group .= $cd['group'];
4601 if ($useGPOS && is_array($cd['GPOSinfo'])) {
4602 $GPOS[$cctr] = $cd['GPOSinfo'];
4603 $GPOS[$cctr]['wDir'] = ($cd['level'] % 2) ? 'RTL' : 'LTR';
4604 }
4605 if($cd['type']==UCDN::BIDI_CLASS_L) { $rtl_content |= 1; }
4606 else if($cd['type']==UCDN::BIDI_CLASS_R) { $rtl_content |= 2; }
4607 $cctr++;
4608 }
4609
4610
4611 $chunkOTLdata['group'] = $group ;
4612 if ($useGPOS) {
4613 $chunkOTLdata['GPOSinfo'] = $GPOS;
4614 }
4615
4616 return array($e,$rtl_content);
4617 }
4618
4619 // **********************************************************************************************
4620 // The following versions for BidiSort work on amalgamated chunks to process the whole paragraph
4621 // Firstly set the level in the OTLdata - called from fn printbuffer() [_bidiPrepare]
4622 // Secondly re-order - called from fn writeFlowingBlock and FinishFlowingBlock, when already divided into lines. [_bidiReorder]
4623 // **********************************************************************************************
4624
4625 function _bidiPrepare(&$para, $dir) {
4626
4627 // Set the initial paragraph embedding level
4628 $pel = 0; // paragraph embedding level
4629 if ($dir == 'rtl') { $pel = 1; }
4630
4631 // X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4632 // Current Embedding Level
4633 $cel = $pel;
4634 // directional override status (-1 is Neutral)
4635 $dos = -1;
4636 $remember = array();
4637 $controlchars = false;
4638 $strongrtl = false;
4639 $diid = 0; // direction isolate ID
4640 $dictr = 0; // direction isolate counter
4641
4642 // Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
4643 // In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
4644 $numchunks = count($para);
4645 for ($nc=0;$nc<$numchunks;$nc++) {
4646 $chunkOTLdata =& $para[$nc][18];
4647
4648 $numchars = count($chunkOTLdata['char_data']);
4649 for ($i=0; $i < $numchars; ++$i) {
4650 if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
4651 // X2. With each RLE, compute the least greater odd embedding level.
4652 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4653 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4654 $next_level = $cel + ($cel % 2) + 1;
4655 if ($next_level < 62) {
4656 $remember[] = array('num' => 8235, 'cel' => $cel, 'dos' => $dos);
4657 $cel = $next_level;
4658 $dos = -1;
4659 $controlchars = true;
4660 }
4661 }
4662 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
4663 // X3. With each LRE, compute the least greater even embedding level.
4664 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4665 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4666 $next_level = $cel + 2 - ($cel % 2);
4667 if ( $next_level < 62 ) {
4668 $remember[] = array('num' => 8234, 'cel' => $cel, 'dos' => $dos);
4669 $cel = $next_level;
4670 $dos = -1;
4671 $controlchars = true;
4672 }
4673 }
4674 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
4675 // X4. With each RLO, compute the least greater odd embedding level.
4676 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to right-to-left.
4677 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4678 $next_level = $cel + ($cel % 2) + 1;
4679 if ($next_level < 62) {
4680 $remember[] = array('num' => 8238, 'cel' => $cel, 'dos' => $dos);
4681 $cel = $next_level;
4682 $dos = UCDN::BIDI_CLASS_R;
4683 $controlchars = true;
4684 }
4685 }
4686 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
4687 // X5. With each LRO, compute the least greater even embedding level.
4688 // a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to left-to-right.
4689 // b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4690 $next_level = $cel + 2 - ($cel % 2);
4691 if ( $next_level < 62 ) {
4692 $remember[] = array('num' => 8237, 'cel' => $cel, 'dos' => $dos);
4693 $cel = $next_level;
4694 $dos = UCDN::BIDI_CLASS_L;
4695 $controlchars = true;
4696 }
4697 }
4698 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
4699 // X7. With each PDF, determine the matching embedding or override code. If there was a valid matching code, restore (pop) the last remembered (pushed) embedding level and directional override.
4700 if (count($remember)) {
4701 $last = count($remember ) - 1;
4702 if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
4703 ($remember[$last]['num'] == 8237)) {
4704 $match = array_pop($remember);
4705 $cel = $match['cel'];
4706 $dos = $match['dos'];
4707 }
4708 }
4709 }
4710 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $chunkOTLdata['char_data'][$i]['uni'] == 8295 ||
4711 $chunkOTLdata['char_data'][$i]['uni'] == 8296) { // LRI // RLI // FSI
4712 // X5a. With each RLI:
4713 // X5b. With each LRI:
4714 // X5c. With each FSI, apply rules P2 and P3 for First Strong character
4715 // Set the RLI/LRI/FSI embedding level to the embedding level of the last entry on the directional status stack.
4716 if ($dos != -1) { $chardir = $dos; }
4717 else { $chardir = $chunkOTLdata['char_data'][$i]['bidi_class']; }
4718 $chunkOTLdata['char_data'][$i]['level'] = $cel;
4719 $chunkOTLdata['char_data'][$i]['type'] = $chardir;
4720 $chunkOTLdata['char_data'][$i]['diid'] = $diid;
4721
4722 $fsi = '';
4723 // X5c. With each FSI, apply rules P2 and P3 within the isolate run for First Strong character
4724 if ($chunkOTLdata['char_data'][$i]['uni'] == 8296) { // FSI
4725 $lvl = 0;
4726 $nc2 = $nc;
4727 $i2 = $i;
4728 while (!($nc2==($numchunks-1) && $i2==((count($para[$nc2][18]['char_data']))-1))) { // while not at end of last chunk
4729 $i2++;
4730 if ($i2 >= count($para[$nc2][18]['char_data'])) {
4731 $nc2++;
4732 $i2 = 0;
4733 }
4734 if ($lvl > 0) { continue; }
4735 if ($para[$nc2][18]['char_data'][$i2]['uni'] == 8294 || $para[$nc2][18]['char_data'][$i2]['uni'] == 8295 || $para[$nc2][18]['char_data'][$i2]['uni'] == 8296) {
4736 $lvl++;
4737 continue;
4738 }
4739 if ($para[$nc2][18]['char_data'][$i2]['uni'] == 8297) {
4740 $lvl--;
4741 if ($lvl < 0) { break; }
4742 }
4743 if ($para[$nc2][18]['char_data'][$i2]['bidi_class'] === UCDN::BIDI_CLASS_L || $para[$nc2][18]['char_data'][$i2]['bidi_class'] == UCDN::BIDI_CLASS_AL || $para[$nc2][18]['char_data'][$i2]['bidi_class'] === UCDN::BIDI_CLASS_R) {
4744 $fsi = $para[$nc2][18]['char_data'][$i2]['bidi_class'];
4745 break;
4746 }
4747 }
4748 // if fsi not found, fsi is same as paragraph embedding level
4749 if (!$fsi && $fsi!==0) {
4750 if ($pel==1) { $fsi = UCDN::BIDI_CLASS_R ; }
4751 else { $fsi = UCDN::BIDI_CLASS_L ; }
4752 }
4753 }
4754
4755 if ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $fsi === UCDN::BIDI_CLASS_L ) { // LRI or FSI-L
4756 // Compute the least even embedding level greater than the embedding level of the last entry on the directional status stack.
4757 $next_level = $cel + 2 - ($cel % 2);
4758 }
4759 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8295 || $fsi == UCDN::BIDI_CLASS_R || $fsi == UCDN::BIDI_CLASS_AL ) { // RLI or FSI-R
4760 // Compute the least odd embedding level greater than the embedding level of the last entry on the directional status stack.
4761 $next_level = $cel + ($cel % 2) + 1;
4762 }
4763
4764
4765 // Increment the isolate count by one, and push an entry consisting of the new embedding level,
4766 // neutral directional override status, and true directional isolate status onto the directional status stack.
4767 $remember[] = array('num' => $chunkOTLdata['char_data'][$i]['uni'], 'cel' => $cel, 'dos' => $dos, 'diid' => $diid);
4768 $cel = $next_level;
4769 $dos = -1;
4770 $diid = ++$dictr; // Set new direction isolate ID after incrementing direction isolate counter
4771
4772 $controlchars = true;
4773 }
4774 else if ($chunkOTLdata['char_data'][$i]['uni'] == 8297) { // PDI
4775 // X6a. With each PDI, perform the following steps:
4776 // Pop the last entry from the directional status stack and decrement the isolate count by one.
4777 while (count($remember)) {
4778 $last = count($remember ) - 1;
4779 if (($remember[$last]['num'] == 8294) || ($remember[$last]['num'] == 8295) || ($remember[$last]['num'] == 8296)) {
4780 $match = array_pop($remember);
4781 $cel = $match['cel'];
4782 $dos = $match['dos'];
4783 $diid = $match['diid'];
4784 break;
4785 }
4786 // End/close any open embedding states not explicitly closed during the isolate
4787 else if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
4788 ($remember[$last]['num'] == 8237)) {
4789 $match = array_pop($remember);
4790 }
4791 }
4792 // In all cases, set the PDI�s level to the embedding level of the last entry on the directional status stack left after the steps above.
4793 // NB The level assigned to an isolate initiator is always the same as that assigned to the matching PDI.
4794 if ($dos != -1) { $chardir = $dos; }
4795 else { $chardir = $chunkOTLdata['char_data'][$i]['bidi_class']; }
4796 $chunkOTLdata['char_data'][$i]['level'] = $cel;
4797 $chunkOTLdata['char_data'][$i]['type'] = $chardir;
4798 $chunkOTLdata['char_data'][$i]['diid'] = $diid;
4799 $controlchars = true;
4800 }
4801 else if ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
4802 // Reset to start values
4803 $cel = $pel;
4804 $dos = -1;
4805 $remember = array();
4806 }
4807 else {
4808 // X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
4809 // a. Set the level of the current character to the current embedding level.
4810 // b. When the directional override status is not neutral, reset the current character type to directional override status.
4811 if ($dos != -1) { $chardir = $dos; }
4812 else {
4813 $chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
4814 if ($chardir == UCDN::BIDI_CLASS_R || $chardir == UCDN::BIDI_CLASS_AL) { $strongrtl = true; }
4815 }
4816 $chunkOTLdata['char_data'][$i]['level'] = $cel;
4817 $chunkOTLdata['char_data'][$i]['type'] = $chardir;
4818 $chunkOTLdata['char_data'][$i]['diid'] = $diid;
4819 }
4820 }
4821 // X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
4822 // Paragraph separators are not included in the embedding.
4823 // X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
4824 if ($controlchars) {
4825 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xaa");
4826 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xab");
4827 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xac");
4828 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xad");
4829 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xae");
4830 preg_replace("/\x{202a}-\x{202e}/u", '', $para[$nc][0]);
4831 }
4832 }
4833
4834 // Remove any blank chunks made by removing directional codes
4835 $numchunks = count($para);
4836 for ($nc=($numchunks-1);$nc>=0;$nc--) {
4837 if (count($para[$nc][18]['char_data'])==0) { array_splice($para, $nc, 1); }
4838 }
4839 if ($dir != 'rtl' && !$strongrtl && !$controlchars) { return; }
4840
4841 $numchunks = count($para);
4842
4843 // X10. Determine the start-of-sequence (sor) and end-of-sequence (eor) types, either L or R, for each isolating run sequence. These depend on the higher of the two levels on either side of the sequence boundary:
4844 // For sor, compare the level of the first character in the sequence with the level of the character preceding it in the paragraph or if there is none, with the paragraph embedding level.
4845 // For eor, compare the level of the last character in the sequence with the level of the character following it in the paragraph or if there is none, with the paragraph embedding level.
4846 // If the higher level is odd, the sor or eor is R; otherwise, it is L.
4847
4848 for ($ir=0; $ir<=$dictr;$ir++) {
4849 $prelevel = $pel;
4850 $postlevel = $pel;
4851 $firstchar = true;
4852 for ($nc=0;$nc<$numchunks;$nc++) {
4853 $chardata =& $para[$nc][18]['char_data'];
4854 $numchars = count($chardata);
4855 for ($i=0; $i < $numchars; ++$i) {
4856 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
4857 $right = $postlevel;
4858 $nc2 = $nc;
4859 $i2 = $i;
4860 while (!($nc2==($numchunks-1) && $i2==((count($para[$nc2][18]['char_data']))-1))) { // while not at end of last chunk
4861 $i2++;
4862 if ($i2 >= count($para[$nc2][18]['char_data'])) {
4863 $nc2++;
4864 $i2 = 0;
4865 }
4866
4867 if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid']==$ir) { $right = $para[$nc2][18]['char_data'][$i2]['level']; break; }
4868 }
4869
4870 $level = $chardata[$i]['level'];
4871 if ($firstchar || $level!=$prelevel) {
4872 $chardata[$i]['sor'] = max($prelevel, $level) % 2 ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
4873 }
4874 if (($nc==($numchunks-1) && $i==($numchars-1)) || $level != $right) {
4875 $chardata[$i]['eor'] = max($right, $level) % 2 ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
4876 }
4877 $prelevel = $level;
4878 $firstchar = false;
4879 }
4880 }
4881 }
4882
4883
4884 // 3.3.3 Resolving Weak Types
4885 // Weak types are now resolved one level run at a time. At level run boundaries where the type of the character on the other side of the boundary is required, the type assigned to sor or eor is used.
4886 // Nonspacing marks are now resolved based on the previous characters.
4887
4888 // W1. Examine each nonspacing mark (NSM) in the level run, and change the type of the NSM to the type of the previous character. If the NSM is at the start of the level run, it will get the type of sor.
4889 for ($ir=0; $ir<=$dictr;$ir++) {
4890 $prevtype = 0;
4891 for ($nc=0;$nc<$numchunks;$nc++) {
4892 $chardata =& $para[$nc][18]['char_data'];
4893 $numchars = count($chardata);
4894 for ($i=0; $i < $numchars; ++$i) {
4895 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
4896 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_NSM) {
4897 if (isset($chardata[$i]['sor'])) {
4898 $chardata[$i]['type'] = $chardata[$i]['sor'];
4899 }
4900 else {
4901 $chardata[$i]['type'] = $prevtype;
4902 }
4903 }
4904 $prevtype = $chardata[$i]['type'];
4905 }
4906 }
4907 }
4908
4909 // W2. Search backward from each instance of a European number until the first strong type (R, L, AL or sor) is found. If an AL is found, change the type of the European number to Arabic number.
4910 for ($ir=0; $ir<=$dictr;$ir++) {
4911 $laststrongtype = -1;
4912 for ($nc=0;$nc<$numchunks;$nc++) {
4913 $chardata =& $para[$nc][18]['char_data'];
4914 $numchars = count($chardata);
4915 for ($i=0; $i < $numchars; ++$i) {
4916 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
4917 if (isset($chardata[$i]['sor'])) { $laststrongtype = $chardata[$i]['sor']; }
4918 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN && $laststrongtype == UCDN::BIDI_CLASS_AL ) {
4919 $chardata[$i]['type'] = UCDN::BIDI_CLASS_AN;
4920 }
4921 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_L || $chardata[$i]['type'] == UCDN::BIDI_CLASS_R || $chardata[$i]['type'] == UCDN::BIDI_CLASS_AL) {
4922 $laststrongtype = $chardata[$i]['type'];
4923 }
4924 }
4925 }
4926 }
4927
4928
4929 // W3. Change all ALs to R.
4930 for ($nc=0;$nc<$numchunks;$nc++) {
4931 $chardata =& $para[$nc][18]['char_data'];
4932 $numchars = count($chardata);
4933 for ($i=0; $i < $numchars; ++$i) {
4934 if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == UCDN::BIDI_CLASS_AL) { $chardata[$i]['type'] = UCDN::BIDI_CLASS_R; }
4935 }
4936 }
4937
4938
4939 // W4. A single European separator between two European numbers changes to a European number. A single common separator between two numbers of the same type changes to that type.
4940 for ($ir=0; $ir<=$dictr;$ir++) {
4941 $prevtype = -1;
4942 $nexttype = -1;
4943 for ($nc=0;$nc<$numchunks;$nc++) {
4944 $chardata =& $para[$nc][18]['char_data'];
4945 $numchars = count($chardata);
4946 for ($i=0; $i < $numchars; ++$i) {
4947 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
4948
4949 // Get next type
4950 $nexttype = -1;
4951 $nc2 = $nc;
4952 $i2 = $i;
4953 while (!($nc2==($numchunks-1) && $i2==((count($para[$nc2][18]['char_data']))-1))) { // while not at end of last chunk
4954 $i2++;
4955 if ($i2 >= count($para[$nc2][18]['char_data'])) {
4956 $nc2++;
4957 $i2 = 0;
4958 }
4959
4960 if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid']==$ir) { $nexttype = $para[$nc2][18]['char_data'][$i2]['type']; break; }
4961 }
4962
4963 if (!isset($chardata[$i]['sor']) && !isset($chardata[$i]['eor'])) {
4964 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ES && $prevtype == UCDN::BIDI_CLASS_EN && $nexttype == UCDN::BIDI_CLASS_EN) {
4965 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4966 }
4967 else if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS && $prevtype == UCDN::BIDI_CLASS_EN && $nexttype == UCDN::BIDI_CLASS_EN) {
4968 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4969 }
4970 else if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS && $prevtype == UCDN::BIDI_CLASS_AN && $nexttype == UCDN::BIDI_CLASS_AN) {
4971 $chardata[$i]['type'] = UCDN::BIDI_CLASS_AN;
4972 }
4973 }
4974 $prevtype = $chardata[$i]['type'];
4975 }
4976 }
4977 }
4978
4979 // W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
4980 for ($ir=0; $ir<=$dictr;$ir++) {
4981 $prevtype = -1;
4982 $nexttype = -1;
4983 for ($nc=0;$nc<$numchunks;$nc++) {
4984 $chardata =& $para[$nc][18]['char_data'];
4985 $numchars = count($chardata);
4986 for ($i=0; $i < $numchars; ++$i) {
4987 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
4988 if (isset($chardata[$i]['sor'])) { $prevtype = $chardata[$i]['sor']; }
4989
4990 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ET) {
4991 if ($prevtype == UCDN::BIDI_CLASS_EN) {
4992 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
4993 }
4994 else if (!isset($chardata[$i]['eor'])) {
4995 $nexttype = -1;
4996 $nc2 = $nc;
4997 $i2 = $i;
4998 while (!($nc2==($numchunks-1) && $i2==((count($para[$nc2][18]['char_data']))-1))) { // while not at end of last chunk
4999 $i2++;
5000 if ($i2 >= count($para[$nc2][18]['char_data'])) {
5001 $nc2++;
5002 $i2 = 0;
5003 }
5004 if ($para[$nc2][18]['char_data'][$i2]['diid']!=$ir) { continue; }
5005 $nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5006 if (isset($para[$nc2][18]['char_data'][$i2]['sor'])) { break; }
5007 if ($nexttype == UCDN::BIDI_CLASS_EN) {
5008 $chardata[$i]['type'] = UCDN::BIDI_CLASS_EN;
5009 break;
5010 }
5011 else if ($nexttype != UCDN::BIDI_CLASS_ET) { break; }
5012 }
5013 }
5014 }
5015 $prevtype = $chardata[$i]['type'];
5016 }
5017 }
5018 }
5019
5020 // W6. Otherwise, separators and terminators change to Other Neutral.
5021 for ($nc=0;$nc<$numchunks;$nc++) {
5022 $chardata =& $para[$nc][18]['char_data'];
5023 $numchars = count($chardata);
5024 for ($i=0; $i < $numchars; ++$i) {
5025 if (isset($chardata[$i]['type']) && (($chardata[$i]['type'] == UCDN::BIDI_CLASS_ET) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ES) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_CS))) {
5026 $chardata[$i]['type'] = UCDN::BIDI_CLASS_ON;
5027 }
5028 }
5029 }
5030
5031 //W7. Search backward from each instance of a European number until the first strong type (R, L, or sor) is found. If an L is found, then change the type of the European number to L.
5032 for ($ir=0; $ir<=$dictr;$ir++) {
5033 $laststrongtype = -1;
5034 for ($nc=0;$nc<$numchunks;$nc++) {
5035 $chardata =& $para[$nc][18]['char_data'];
5036 $numchars = count($chardata);
5037 for ($i=0; $i < $numchars; ++$i) {
5038 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
5039 if (isset($chardata[$i]['sor'])) { $laststrongtype = $chardata[$i]['sor']; }
5040 if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == UCDN::BIDI_CLASS_EN && $laststrongtype == UCDN::BIDI_CLASS_L ) {
5041 $chardata[$i]['type'] = UCDN::BIDI_CLASS_L;
5042 }
5043 if (isset($chardata[$i]['type']) && ($chardata[$i]['type'] == UCDN::BIDI_CLASS_L || $chardata[$i]['type'] == UCDN::BIDI_CLASS_R || $chardata[$i]['type'] == UCDN::BIDI_CLASS_AL)) {
5044 $laststrongtype = $chardata[$i]['type'];
5045 }
5046 }
5047 }
5048 }
5049
5050 // N1. A sequence of neutrals takes the direction of the surrounding strong text if the text on both sides has the same direction. European and Arabic numbers act as if they were R in terms of their influence on neutrals. Start-of-level-run (sor) and end-of-level-run (eor) are used at level run boundaries.
5051 for ($ir=0; $ir<=$dictr;$ir++) {
5052 $laststrongtype = -1;
5053 for ($nc=0;$nc<$numchunks;$nc++) {
5054 $chardata =& $para[$nc][18]['char_data'];
5055 $numchars = count($chardata);
5056 for ($i=0; $i < $numchars; ++$i) {
5057 if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid']!=$ir) { continue; } // Ignore characters in a different isolate run
5058 if (isset($chardata[$i]['sor'])) { $laststrongtype = $chardata[$i]['sor']; }
5059 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ON || $chardata[$i]['type'] == UCDN::BIDI_CLASS_WS) {
5060 $left = -1;
5061 // LEFT
5062 if ($laststrongtype == UCDN::BIDI_CLASS_R || $laststrongtype == UCDN::BIDI_CLASS_EN || $laststrongtype == UCDN::BIDI_CLASS_AN) {
5063 $left = UCDN::BIDI_CLASS_R;
5064 }
5065 else if ($laststrongtype == UCDN::BIDI_CLASS_L) {
5066 $left = UCDN::BIDI_CLASS_L;
5067 }
5068 // RIGHT
5069 $right = -1;
5070 // move to the right of any following neutrals OR hit a run boundary
5071
5072 if (isset($chardata[$i]['eor'])) {
5073 $right = $chardata[$i]['eor'];
5074 }
5075 else {
5076 $nexttype = -1;
5077 $nc2 = $nc;
5078 $i2 = $i;
5079 while (!($nc2==($numchunks-1) && $i2==((count($para[$nc2][18]['char_data']))-1))) { // while not at end of last chunk
5080 $i2++;
5081 if ($i2 >= count($para[$nc2][18]['char_data'])) {
5082 $nc2++;
5083 $i2 = 0;
5084 }
5085 if (!isset($para[$nc2][18]['char_data'][$i2]['diid']) || $para[$nc2][18]['char_data'][$i2]['diid']!=$ir) { continue; }
5086 $nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5087 if ($nexttype == UCDN::BIDI_CLASS_R || $nexttype == UCDN::BIDI_CLASS_EN || $nexttype == UCDN::BIDI_CLASS_AN) {
5088 $right = UCDN::BIDI_CLASS_R;
5089 break;
5090 }
5091 else if ($nexttype == UCDN::BIDI_CLASS_L) {
5092 $right = UCDN::BIDI_CLASS_L;
5093 break;
5094 }
5095 else if (isset($para[$nc2][18]['char_data'][$i2]['eor'])) {
5096 $right = $para[$nc2][18]['char_data'][$i2]['eor'];
5097 break;
5098 }
5099 }
5100 }
5101
5102 if ($left > -1 && $left==$right) {
5103 $chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5104 $chardata[$i]['type'] = $left;
5105 }
5106 }
5107 else if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_L || $chardata[$i]['type'] == UCDN::BIDI_CLASS_R || $chardata[$i]['type'] == UCDN::BIDI_CLASS_EN || $chardata[$i]['type'] == UCDN::BIDI_CLASS_AN) {
5108 $laststrongtype = $chardata[$i]['type'];
5109 }
5110 }
5111 }
5112 }
5113
5114 // N2. Any remaining neutrals take the embedding direction
5115 for ($nc=0;$nc<$numchunks;$nc++) {
5116 $chardata =& $para[$nc][18]['char_data'];
5117 $numchars = count($chardata);
5118 for ($i=0; $i < $numchars; ++$i) {
5119 if (isset($chardata[$i]['type']) && ($chardata[$i]['type'] == UCDN::BIDI_CLASS_ON || $chardata[$i]['type'] == UCDN::BIDI_CLASS_WS)) {
5120 $chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5121 $chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? UCDN::BIDI_CLASS_R : UCDN::BIDI_CLASS_L;
5122 }
5123 }
5124 }
5125
5126 // I1. For all characters with an even (left-to-right) embedding direction, those of type R go up one level and those of type AN or EN go up two levels.
5127 // I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
5128 for ($nc=0;$nc<$numchunks;$nc++) {
5129 $chardata =& $para[$nc][18]['char_data'];
5130 $numchars = count($chardata);
5131 for ($i=0; $i < $numchars; ++$i) {
5132 if (isset($chardata[$i]['level'])) {
5133 $odd = $chardata[$i]['level'] % 2;
5134 if ($odd) {
5135 if (($chardata[$i]['type'] == UCDN::BIDI_CLASS_L) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_AN) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN)) {
5136 $chardata[$i]['level'] += 1;
5137 }
5138 }
5139 else {
5140 if ($chardata[$i]['type'] == UCDN::BIDI_CLASS_R) { $chardata[$i]['level'] += 1; }
5141 else if (($chardata[$i]['type'] == UCDN::BIDI_CLASS_AN) || ($chardata[$i]['type'] == UCDN::BIDI_CLASS_EN)) { $chardata[$i]['level'] += 2; }
5142 }
5143 }
5144 }
5145 }
5146
5147 // Remove Isolate formatters
5148 $numchunks = count($para);
5149 if ($controlchars) {
5150 for ($nc=0;$nc<$numchunks;$nc++) {
5151 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa6");
5152 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa7");
5153 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa8");
5154 $this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa9");
5155 preg_replace("/\x{2066}-\x{2069}/u", '', $para[$nc][0]);
5156 }
5157 // Remove any blank chunks made by removing directional codes
5158 for ($nc=($numchunks-1);$nc>=0;$nc--) {
5159 if (count($para[$nc][18]['char_data'])==0) { array_splice($para, $nc, 1); }
5160 }
5161 }
5162
5163 }
5164
5165
5166
5167 // Reorder, once divided into lines
5168
5169 function _bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir) {
5170
5171 $bidiData = array();
5172
5173 // First combine into one array (and get the highest level in use)
5174 $numchunks = count($content);
5175 $maxlevel = 0;
5176 for ($nc=0;$nc<$numchunks;$nc++) {
5177 $numchars = count($cOTLdata[$nc]['char_data']);
5178 for ($i=0; $i < $numchars; ++$i) {
5179
5180 $carac = array();
5181 if (isset($cOTLdata[$nc]['GPOSinfo'][$i])) {$carac['GPOSinfo'] = $cOTLdata[$nc]['GPOSinfo'][$i]; }
5182 $carac['uni'] = $cOTLdata[$nc]['char_data'][$i]['uni'];
5183 if (isset($cOTLdata[$nc]['char_data'][$i]['type'])) $carac['type'] = $cOTLdata[$nc]['char_data'][$i]['type'];
5184 if (isset($cOTLdata[$nc]['char_data'][$i]['level'])) $carac['level'] = $cOTLdata[$nc]['char_data'][$i]['level'];
5185 if (isset($cOTLdata[$nc]['char_data'][$i]['orig_type'])) { $carac['orig_type'] = $cOTLdata[$nc]['char_data'][$i]['orig_type']; }
5186 $carac['group'] = $cOTLdata[$nc]['group']{$i};
5187 $carac['chunkid'] = $chunkorder[$nc]; // gives font id and/or object ID
5188
5189 $maxlevel = max((isset($carac['level']) ? $carac['level'] : 0),$maxlevel);
5190 $bidiData[] = $carac;
5191 }
5192 }
5193 if ($maxlevel==0) { return; }
5194
5195 $numchars = count($bidiData);
5196
5197 // L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
5198 // 1. Segment separators (Tab) 'S',
5199 // 2. Paragraph separators 'B',
5200 // 3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
5201 // 4. Any sequence of whitespace characters 'WS' at the end of the line.
5202 // The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
5203 // Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
5204
5205 // Set the initial paragraph embedding level
5206 if ($blockdir == 'rtl') { $pel = 1; }
5207 else { $pel = 0; }
5208
5209 for ($i=($numchars-1); $i>0; $i--) {
5210 if ($bidiData[$i]['type'] == UCDN::BIDI_CLASS_WS || (isset($bidiData[$i]['orig_type']) && $bidiData[$i]['orig_type'] == UCDN::BIDI_CLASS_WS)) {
5211 $bidiData[$i]['level'] = $pel;
5212 }
5213 else { break; }
5214 }
5215
5216
5217 // L2. From the highest level found in the text to the lowest odd level on each line, including intermediate levels not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
5218 for ($j=$maxlevel; $j > 0; $j--) {
5219 $ordarray = array();
5220 $revarr = array();
5221 $onlevel = false;
5222 for ($i=0; $i < $numchars; ++$i) {
5223 if ($bidiData[$i]['level'] >= $j) {
5224 $onlevel = true;
5225 // L4. A character is depicted by a mirrored glyph if and only if (a) the resolved directionality of that character is R, and (b) the Bidi_Mirrored property value of that character is true.
5226 if (isset(UCDN::$mirror_pairs[$bidiData[$i]['uni']]) && $bidiData[$i]['type']==UCDN::BIDI_CLASS_R) {
5227 $bidiData[$i]['uni'] = UCDN::$mirror_pairs[$bidiData[$i]['uni']];
5228 }
5229
5230 $revarr[] = $bidiData[$i];
5231 }
5232 else {
5233 if ($onlevel) {
5234 $revarr = array_reverse($revarr);
5235 $ordarray = array_merge($ordarray, $revarr);
5236 $revarr = Array();
5237 $onlevel = false;
5238 }
5239 $ordarray[] = $bidiData[$i];
5240 }
5241 }
5242 if ($onlevel) {
5243 $revarr = array_reverse($revarr);
5244 $ordarray = array_merge($ordarray, $revarr);
5245 }
5246 $bidiData = $ordarray;
5247 }
5248
5249 $content = array();
5250 $cOTLdata = array();
5251 $chunkorder = array();
5252
5253
5254
5255 $nc = -1; // New chunk order ID
5256 $chunkid = -1;
5257
5258 foreach ($bidiData as $carac) {
5259 if ($carac['chunkid'] != $chunkid) {
5260 $nc++;
5261 $chunkorder[$nc] = $carac['chunkid'];
5262 $cctr = 0;
5263 $content[$nc] = '';
5264 $cOTLdata[$nc]['group'] = '';
5265 }
5266 if ($carac['uni'] != 0xFFFC) { // Object replacement character (65532)
5267 $content[$nc] .= code2utf($carac['uni']);
5268 $cOTLdata[$nc]['group'] .= $carac['group'];
5269 if (!empty($carac['GPOSinfo'])) {
5270 if (isset($carac['GPOSinfo'])) { $cOTLdata[$nc]['GPOSinfo'][$cctr] = $carac['GPOSinfo']; }
5271 $cOTLdata[$nc]['GPOSinfo'][$cctr]['wDir'] = ($carac['level'] % 2) ? 'RTL' : 'LTR';
5272 }
5273 }
5274 $chunkid = $carac['chunkid'];
5275 $cctr++;
5276 }
5277
5278 }
5279
5280
5281
5282
5283
5284
5285 ////////////////////////////////////////////////////////////////
5286 ////////////////////////////////////////////////////////////////
5287 // These functions are called from mpdf after GSUB/GPOS has taken place
5288 // At this stage the bidi-type is in string form
5289 ////////////////////////////////////////////////////////////////
5290 ////////////////////////////////////////////////////////////////
5291 function splitOTLdata(&$cOTLdata, $OTLcutoffpos, $OTLrestartpos='') {
5292 if (!$OTLrestartpos) { $OTLrestartpos = $OTLcutoffpos; }
5293 $newOTLdata = array('GPOSinfo' => array(), 'char_data' => array());
5294 $newOTLdata['group'] = substr($cOTLdata['group'],$OTLrestartpos);
5295 $cOTLdata['group'] = substr($cOTLdata['group'],0,$OTLcutoffpos);
5296
5297 if (isset($cOTLdata['GPOSinfo']) && $cOTLdata['GPOSinfo']) {
5298 foreach($cOTLdata['GPOSinfo'] AS $k => $val) {
5299 if ($k >= $OTLrestartpos) {
5300 $newOTLdata['GPOSinfo'][($k - $OTLrestartpos)] = $val;
5301 }
5302 if ($k >= $OTLcutoffpos) {
5303 unset($cOTLdata['GPOSinfo'][$k]);
5304 //$cOTLdata['GPOSinfo'][$k] = array();
5305 }
5306 }
5307 }
5308 if (isset($cOTLdata['char_data'])) {
5309 $newOTLdata['char_data'] = array_slice($cOTLdata['char_data'], $OTLrestartpos);
5310 array_splice($cOTLdata['char_data'], $OTLcutoffpos);
5311 }
5312
5313 // Not necessary - easier to debug
5314 if (isset($cOTLdata['GPOSinfo'])) ksort($cOTLdata['GPOSinfo']);
5315 if (isset($newOTLdata['GPOSinfo'])) ksort($newOTLdata['GPOSinfo']);
5316
5317 return $newOTLdata;
5318 }
5319
5320 function sliceOTLdata($OTLdata, $pos, $len) {
5321 $newOTLdata = array('GPOSinfo' => array(), 'char_data' => array());
5322 $newOTLdata['group'] = substr($OTLdata['group'],$pos,$len);
5323
5324 if ($OTLdata['GPOSinfo']) {
5325 foreach($OTLdata['GPOSinfo'] AS $k => $val) {
5326 if ($k >= $pos && $k <($pos+$len)) {
5327 $newOTLdata['GPOSinfo'][($k - $pos)] = $val;
5328 }
5329 }
5330 }
5331
5332 if (isset($OTLdata['char_data'])) { $newOTLdata['char_data'] = array_slice($OTLdata['char_data'], $pos, $len); }
5333
5334 // Not necessary - easier to debug
5335 if ($newOTLdata['GPOSinfo']) ksort($newOTLdata['GPOSinfo']);
5336
5337 return $newOTLdata;
5338 }
5339
5340 // Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5341 function removeChar(&$txt, &$cOTLdata, $char) {
5342 while(mb_strpos($txt, $char, 0, $this->mpdf->mb_enc )!== false) {
5343 $pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc );
5344 $newGPOSinfo = array();
5345 $cOTLdata['group'] = substr_replace($cOTLdata['group'], '', $pos, 1);
5346 if ($cOTLdata['GPOSinfo']) {
5347 foreach($cOTLdata['GPOSinfo'] AS $k => $val) {
5348 if ($k > $pos) {
5349 $newGPOSinfo[($k - 1)] = $val;
5350 }
5351 else if ($k!=$pos) {
5352 $newGPOSinfo[$k] = $val;
5353 }
5354 }
5355 $cOTLdata['GPOSinfo'] = $newGPOSinfo;
5356 }
5357 if (isset($cOTLdata['char_data'])) { array_splice($cOTLdata['char_data'], $pos, 1); }
5358
5359 $txt = preg_replace("/".$char."/",'',$txt, 1);
5360 }
5361 }
5362
5363 // Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5364 function replaceSpace(&$txt, &$cOTLdata) {
5365 $char = chr(194).chr(160); // NBSP
5366 while(mb_strpos($txt, $char, 0, $this->mpdf->mb_enc )!== false) {
5367 $pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc );
5368 if ($cOTLdata['char_data'][$pos]['uni'] == 160) {
5369 $cOTLdata['char_data'][$pos]['uni'] = 32;
5370 }
5371 $txt = preg_replace("/".$char."/",' ',$txt, 1);
5372 }
5373 }
5374
5375 function trimOTLdata(&$cOTLdata, $Left=true, $Right=true) {
5376
5377 $len = count($cOTLdata['char_data']);
5378 $nLeft = 0;
5379 $nRight = 0;
5380 for($i=0;$i<$len;$i++) {
5381 if($cOTLdata['char_data'][$i]['uni']==32 || $cOTLdata['char_data'][$i]['uni']==12288) { $nLeft++; } // 12288 = 0x3000 = CJK space
5382 else { break; }
5383 }
5384 for($i=($len-1);$i>=0;$i--) {
5385 if($cOTLdata['char_data'][$i]['uni']==32 || $cOTLdata['char_data'][$i]['uni']==12288) { $nRight++; } // 12288 = 0x3000 = CJK space
5386 else { break; }
5387 }
5388
5389 // Trim Right
5390 if ($Right && $nRight) {
5391 $cOTLdata['group'] = substr($cOTLdata['group'],0,strlen($cOTLdata['group'])-$nRight);
5392 if ($cOTLdata['GPOSinfo']) {
5393 foreach($cOTLdata['GPOSinfo'] AS $k => $val) {
5394 if ($k >= $len-$nRight) {
5395 unset($cOTLdata['GPOSinfo'][$k]);
5396 }
5397 }
5398 }
5399 if (isset($cOTLdata['char_data'])) {
5400 for($i=0;$i<$nRight;$i++) {
5401 array_pop($cOTLdata['char_data']);
5402 }
5403 }
5404 }
5405 // Trim Left
5406 if ($Left && $nLeft) {
5407 $cOTLdata['group'] = substr($cOTLdata['group'],$nLeft);
5408 if ($cOTLdata['GPOSinfo']) {
5409 $newPOSinfo = array();
5410 foreach($cOTLdata['GPOSinfo'] AS $k => $val) {
5411 if ($k >= $nLeft) {
5412 $newPOSinfo[$k-$nLeft] = $cOTLdata['GPOSinfo'][$k];
5413 }
5414 }
5415 $cOTLdata['GPOSinfo'] = $newPOSinfo;
5416 }
5417 if (isset($cOTLdata['char_data'])) {
5418 for($i=0;$i<$nLeft;$i++) {
5419 array_shift($cOTLdata['char_data']);
5420 }
5421 }
5422 }
5423 }
5424
5425
5426 ////////////////////////////////////////////////////////////////
5427 ////////////////////////////////////////////////////////////////
5428 ////////// GENERAL OTL FUNCTIONS /////////////////
5429 ////////////////////////////////////////////////////////////////
5430 ////////////////////////////////////////////////////////////////
5431
5432
5433 function glyphToChar($gid) {
5434 return (ord($this->glyphIDtoUni[$gid*3]) << 16) + (ord($this->glyphIDtoUni[$gid*3+1]) << 8) + ord($this->glyphIDtoUni[$gid*3+2]);
5435 }
5436
5437 function unicode_hex($unicode_dec) {
5438 return (str_pad(strtoupper(dechex($unicode_dec)),5,'0',STR_PAD_LEFT));
5439 }
5440
5441 function seek($pos) {
5442 $this->_pos = $pos;
5443 }
5444
5445 function skip($delta) {
5446 $this->_pos += $delta;
5447 }
5448 function read_short() {
5449 $a = (ord($this->ttfOTLdata[$this->_pos])<<8) + ord($this->ttfOTLdata[$this->_pos+1]);
5450 if ($a & (1 << 15) ) {
5451 $a = ($a - (1 << 16));
5452 }
5453 $this->_pos += 2;
5454 return $a;
5455 }
5456
5457 function read_ushort() {
5458 $a = (ord($this->ttfOTLdata[$this->_pos])<<8) + ord($this->ttfOTLdata[$this->_pos+1]);
5459 $this->_pos += 2;
5460 return $a;
5461 }
5462
5463
5464 function _getCoverageGID() {
5465 // Called from Lookup Type 1, Format 1 - returns glyphIDs rather than hexstrings
5466 // Need to do this separately to cache separately
5467 // Otherwise the same as fn below _getCoverage
5468 $offset = $this->_pos;
5469 if (isset($this->LuDataCache[$this->fontkey]['GID'][$offset])) {
5470 $g = $this->LuDataCache[$this->fontkey]['GID'][$offset];
5471 }
5472 else {
5473 $g = array();
5474 $CoverageFormat= $this->read_ushort();
5475 if ($CoverageFormat == 1) {
5476 $CoverageGlyphCount= $this->read_ushort();
5477 for ($gid=0;$gid<$CoverageGlyphCount;$gid++) {
5478 $glyphID = $this->read_ushort();
5479 $g[] = $glyphID;
5480 }
5481 }
5482 if ($CoverageFormat == 2) {
5483 $RangeCount= $this->read_ushort();
5484 for ($r=0;$r<$RangeCount;$r++) {
5485 $start = $this->read_ushort();
5486 $end = $this->read_ushort();
5487 $StartCoverageIndex = $this->read_ushort(); // n/a
5488 for ($glyphID=$start;$glyphID<=$end;$glyphID++) {
5489 $g[] = $glyphID;
5490 }
5491 }
5492 }
5493 $this->LuDataCache[$this->fontkey]['GID'][$offset] = $g;
5494 }
5495 return $g;
5496 }
5497
5498
5499 function _getCoverage() {
5500 $offset = $this->_pos;
5501 if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5502 $g = $this->LuDataCache[$this->fontkey][$offset];
5503 }
5504 else {
5505 $g = array();
5506 $CoverageFormat= $this->read_ushort();
5507 if ($CoverageFormat == 1) {
5508 $CoverageGlyphCount= $this->read_ushort();
5509 for ($gid=0;$gid<$CoverageGlyphCount;$gid++) {
5510 $glyphID = $this->read_ushort();
5511 $g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5512 }
5513 }
5514 if ($CoverageFormat == 2) {
5515 $RangeCount= $this->read_ushort();
5516 for ($r=0;$r<$RangeCount;$r++) {
5517 $start = $this->read_ushort();
5518 $end = $this->read_ushort();
5519 $StartCoverageIndex = $this->read_ushort(); // n/a
5520 for ($glyphID=$start;$glyphID<=$end;$glyphID++) {
5521 $g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5522 }
5523 }
5524 }
5525 $this->LuDataCache[$this->fontkey][$offset] = $g;
5526 }
5527 return $g;
5528 }
5529
5530 function _getClasses($offset) {
5531 if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5532 $GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
5533 }
5534 else {
5535 $this->seek($offset);
5536 $ClassFormat = $this->read_ushort();
5537 $GlyphByClass = array();
5538 if ($ClassFormat == 1) {
5539 $StartGlyph = $this->read_ushort();
5540 $GlyphCount = $this->read_ushort();
5541 for ($i=0;$i<$GlyphCount;$i++) {
5542 $startGlyphID = $StartGlyph + $i;
5543 $endGlyphID = $StartGlyph + $i;
5544 $class = $this->read_ushort();
5545 // Note: Font FreeSerif , tag "blws"
5546 // $BacktrackClasses[0] is defined ? a mistake in the font ???
5547 // Let's ignore for now
5548 if ($class > 0) {
5549 for($g=$startGlyphID;$g<=$endGlyphID;$g++) {
5550 if ($this->glyphToChar($g)) {
5551 $GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5552 }
5553 }
5554 }
5555 }
5556 }
5557 else if ($ClassFormat == 2) {
5558 $tableCount = $this->read_ushort();
5559 for ($i=0;$i<$tableCount;$i++) {
5560 $startGlyphID = $this->read_ushort();
5561 $endGlyphID = $this->read_ushort();
5562 $class = $this->read_ushort();
5563 // Note: Font FreeSerif , tag "blws"
5564 // $BacktrackClasses[0] is defined ? a mistake in the font ???
5565 // Let's ignore for now
5566 if ($class > 0) {
5567 for($g=$startGlyphID;$g<=$endGlyphID;$g++) {
5568 if ($this->glyphToChar($g)) {
5569 $GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5570 }
5571 }
5572 }
5573 }
5574 }
5575 $this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
5576 }
5577 return $GlyphByClass;
5578 }
5579
5580
5581 function _getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $shaper, $useOTL, $mode) {
5582 // ScriptLang is the array of available script/lang tags supported by the font
5583 // $scriptblock is the (number/code) for the script of the actual text string based on Unicode properties (UCDN::$uni_scriptblock)
5584 // $scripttag is the default tag derived from $scriptblock
5585 /*
5586 http://www.microsoft.com/typography/otspec/ttoreg.htm
5587 http://www.microsoft.com/typography/otspec/scripttags.htm
5588
5589 Values for useOTL
5590
5591 Bit dn hn Value
5592 1 1 0x0001 GSUB/GPOS - Latin scripts
5593 2 2 0x0002 GSUB/GPOS - Cyrillic scripts
5594 3 4 0x0004 GSUB/GPOS - Greek scripts
5595 4 8 0x0008 GSUB/GPOS - CJK scripts (excluding Hangul-Jamo)
5596 5 16 0x0010 (Reserved)
5597 6 32 0x0020 (Reserved)
5598 7 64 0x0040 (Reserved)
5599 8 128 0x0080 GSUB/GPOS - All other scripts (including all RTL scripts, complex scripts with shapers etc)
5600
5601 NB If change for RTL - cf. function magic_reverse_dir in mpdf.php to update
5602
5603 */
5604
5605
5606 if ($scriptblock == UCDN::SCRIPT_LATIN) {
5607 if (!($useOTL & 0x01)) { return array('',false); }
5608 }
5609 else if ($scriptblock == UCDN::SCRIPT_CYRILLIC) {
5610 if (!($useOTL & 0x02)) { return array('',false); }
5611 }
5612 else if ($scriptblock == UCDN::SCRIPT_GREEK) {
5613 if (!($useOTL & 0x04)) { return array('',false); }
5614 }
5615 else if ($scriptblock >= UCDN::SCRIPT_HIRAGANA && $scriptblock <= UCDN::SCRIPT_YI ) {
5616 if (!($useOTL & 0x08)) { return array('',false); }
5617 }
5618 else {
5619 if (!($useOTL & 0x80)) { return array('',false); }
5620 }
5621
5622 // If availabletags includes scripttag - choose
5623 if (isset($ScriptLang[$scripttag])) { return array($scripttag, false); }
5624
5625 // If INDIC (or Myanmar) and available tag not includes new version, check if includes old version & choose old version
5626 if ($shaper) {
5627 switch($scripttag) {
5628 CASE 'bng2': if (isset($ScriptLang['beng'])) return array('beng',true);
5629 CASE 'dev2': if (isset($ScriptLang['deva'])) return array('deva',true);
5630 CASE 'gjr2': if (isset($ScriptLang['gujr'])) return array('gujr',true);
5631 CASE 'gur2': if (isset($ScriptLang['guru'])) return array('guru',true);
5632 CASE 'knd2': if (isset($ScriptLang['knda'])) return array('knda',true);
5633 CASE 'mlm2': if (isset($ScriptLang['mlym'])) return array('mlym',true);
5634 CASE 'ory2': if (isset($ScriptLang['orya'])) return array('orya',true);
5635 CASE 'tml2': if (isset($ScriptLang['taml'])) return array('taml',true);
5636 CASE 'tel2': if (isset($ScriptLang['telu'])) return array('telu',true);
5637 CASE 'mym2': if (isset($ScriptLang['mymr'])) return array('mymr',true);
5638 }
5639 }
5640
5641 // choose DFLT if present
5642 if (isset($ScriptLang['DFLT'])) { return array('DFLT', false); }
5643 // else choose dflt if present
5644 if (isset($ScriptLang['dflt'])) { return array('dflt', false); }
5645 // else return no scriptTag
5646 if (isset($ScriptLang['latn'])) { return array('latn', false); }
5647 // else return no scriptTag
5648 return array('',false);
5649
5650 }
5651
5652
5653
5654 // LangSys tags
5655 function _getOTLLangTag($ietf, $available) {
5656 // http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
5657 // http://www.microsoft.com/typography/otspec/languagetags.htm
5658 // IETF tag = e.g. en-US, und-Arab, sr-Cyrl cf. config_lang2fonts.php
5659 if ($available=='') { return ''; }
5660 $tags = preg_split('/-/',$ietf);
5661 $lang = '';
5662 $country = '';
5663 $script = '';
5664 $lang = strtolower($tags[0]);
5665 if (isset($tags[1]) && $tags[1]) {
5666 if (strlen($tags[1]) == 2) { $country = strtolower($tags[1]); }
5667 }
5668 if (isset($tags[2]) && $tags[2]) { $country = strtolower($tags[2]); }
5669
5670 if ($lang!='' && isset(UCDN::$ot_languages[$lang])) { $langsys = UCDN::$ot_languages[$lang]; }
5671 else if ($lang!='' && $country !='' && isset(UCDN::$ot_languages[$lang.''.$country])) {
5672 $langsys = UCDN::$ot_languages[$lang.''.$country];
5673 }
5674 else { $langsys = "DFLT"; }
5675 if (strpos($available, $langsys)===false) {
5676 if (strpos($available, "DFLT")!==false) { return "DFLT"; }
5677 else return '';
5678 }
5679 return $langsys;
5680 }
5681
5682 function _dumpproc($GPOSSUB, $lookupID, $subtable, $Type, $Format, $ptr, $currGlyph, $level) {
5683 echo '<div style="padding-left: '.($level*2).'em;">';
5684 echo $GPOSSUB .' LookupID #'.$lookupID.' Subtable#'.$subtable .' Type: '.$Type.' Format: '.$Format.'<br />';
5685 echo '<div style="font-family:monospace">';
5686 echo 'Glyph position: '.$ptr.' Current Glyph: '.$currGlyph.'<br />';
5687
5688 for ($i=0;$i<count($this->OTLdata);$i++) {
5689 if ($i==$ptr) { echo '<b>'; }
5690 echo $this->OTLdata[$i]['hex'] . ' ';
5691 if ($i==$ptr) { echo '</b>'; }
5692 }
5693 echo '<br />';
5694
5695 for ($i=0;$i<count($this->OTLdata);$i++) {
5696 if ($i==$ptr) { echo '<b>'; }
5697 echo str_pad($this->OTLdata[$i]['uni'],5) . ' ';
5698 if ($i==$ptr) { echo '</b>'; }
5699 }
5700 echo '<br />';
5701
5702 if ($GPOSSUB == 'GPOS') {
5703 for ($i=0;$i<count($this->OTLdata);$i++) {
5704 if (!empty($this->OTLdata[$i]['GPOSinfo'])) {
5705 echo $this->OTLdata[$i]['hex'] . ' &#x'.$this->OTLdata[$i]['hex'].'; ';
5706 print_r($this->OTLdata[$i]['GPOSinfo']);
5707 echo ' ';
5708 }
5709 }
5710 }
5711
5712 echo '</div>';
5713 echo '</div>';
5714 }
5715
5716
5717 }
5718
5719 ?>