You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1865 lines
66KB

  1. <?php
  2. require_once __DIR__ . '/../MpdfException.php';
  3. class INDIC
  4. {
  5. /* FROM hb-ot-shape-complex-indic-private.hh */
  6. // indic_category
  7. const OT_X = 0;
  8. const OT_C = 1;
  9. const OT_V = 2;
  10. const OT_N = 3;
  11. const OT_H = 4;
  12. const OT_ZWNJ = 5;
  13. const OT_ZWJ = 6;
  14. const OT_M = 7; /* Matra or Dependent Vowel */
  15. const OT_SM = 8;
  16. const OT_VD = 9;
  17. const OT_A = 10;
  18. const OT_NBSP = 11;
  19. const OT_DOTTEDCIRCLE = 12; /* Not in the spec, but special in Uniscribe. /Very very/ special! */
  20. const OT_RS = 13; /* Register Shifter, used in Khmer OT spec */
  21. const OT_Coeng = 14;
  22. const OT_Repha = 15;
  23. const OT_Ra = 16; /* Not explicitly listed in the OT spec, but used in the grammar. */
  24. const OT_CM = 17;
  25. // Based on indic_category used to make string to find syllables
  26. // OT_ to string character (using e.g. OT_C from INDIC) hb-ot-shape-complex-indic-private.hh
  27. public static $indic_category_char = array(
  28. 'x',
  29. 'C',
  30. 'V',
  31. 'N',
  32. 'H',
  33. 'Z',
  34. 'J',
  35. 'M',
  36. 'S',
  37. 'v',
  38. 'A', /* Spec gives Andutta U+0952 as OT_A. However, testing shows that Uniscribe
  39. * treats U+0951..U+0952 all as OT_VD - see set_indic_properties */
  40. 's',
  41. 'D',
  42. 'F', /* Register shift Khmer only */
  43. 'G', /* Khmer only */
  44. 'r', /* 0D4E (dot reph) only one in Malayalam */
  45. 'R',
  46. 'm', /* Consonant medial only used in Indic 0A75 in Gurmukhi (0A00..0A7F) : also in Lao, Myanmar, Tai Tham, Javanese & Cham */
  47. );
  48. /* Visual positions in a syllable from left to right. */
  49. /* FROM hb-ot-shape-complex-indic-private.hh */
  50. // indic_position
  51. const POS_START = 0;
  52. const POS_RA_TO_BECOME_REPH = 1;
  53. const POS_PRE_M = 2;
  54. const POS_PRE_C = 3;
  55. const POS_BASE_C = 4;
  56. const POS_AFTER_MAIN = 5;
  57. const POS_ABOVE_C = 6;
  58. const POS_BEFORE_SUB = 7;
  59. const POS_BELOW_C = 8;
  60. const POS_AFTER_SUB = 9;
  61. const POS_BEFORE_POST = 10;
  62. const POS_POST_C = 11;
  63. const POS_AFTER_POST = 12;
  64. const POS_FINAL_C = 13;
  65. const POS_SMVD = 14;
  66. const POS_END = 15;
  67. /*
  68. * Basic features.
  69. * These features are applied in order, one at a time, after initial_reordering.
  70. */
  71. /*
  72. * Must be in the same order as the indic_features array. Ones starting with _ are F_GLOBAL
  73. * Ones without the _ are only applied where the mask says!
  74. */
  75. const _NUKT = 0;
  76. const _AKHN = 1;
  77. const RPHF = 2;
  78. const _RKRF = 3;
  79. const PREF = 4;
  80. const BLWF = 5;
  81. const HALF = 6;
  82. const ABVF = 7;
  83. const PSTF = 8;
  84. const CFAR = 9; // Khmer only
  85. const _VATU = 10;
  86. const _CJCT = 11;
  87. const INIT = 12;
  88. public static function set_indic_properties(&$info, $scriptblock)
  89. {
  90. $u = $info['uni'];
  91. $type = self::indic_get_categories($u);
  92. $cat = ($type & 0x7F);
  93. $pos = ($type >> 8);
  94. /*
  95. * Re-assign category
  96. */
  97. if ($u == 0x17D1)
  98. $cat = self::OT_X;
  99. if ($cat == self::OT_X && self::in_range($u, 0x17CB, 0x17D3)) { /* Khmer Various signs */
  100. /* These are like Top Matras. */
  101. $cat = self::OT_M;
  102. $pos = self::POS_ABOVE_C;
  103. }
  104. if ($u == 0x17C6)
  105. $cat = self::OT_N; /* Khmer Bindu doesn't like to be repositioned. */
  106. if ($u == 0x17D2)
  107. $cat = self::OT_Coeng; /* Khmer coeng */
  108. /* The spec says U+0952 is OT_A. However, testing shows that Uniscribe
  109. * treats U+0951..U+0952 all as OT_VD.
  110. * TESTS:
  111. * U+092E,U+0947,U+0952
  112. * U+092E,U+0952,U+0947
  113. * U+092E,U+0947,U+0951
  114. * U+092E,U+0951,U+0947
  115. * */
  116. //if ($u == 0x0952) $cat = self::OT_A;
  117. if (self::in_range($u, 0x0951, 0x0954))
  118. $cat = self::OT_VD;
  119. if ($u == 0x200C)
  120. $cat = self::OT_ZWNJ;
  121. else if ($u == 0x200D)
  122. $cat = self::OT_ZWJ;
  123. else if ($u == 0x25CC)
  124. $cat = self::OT_DOTTEDCIRCLE;
  125. else if ($u == 0x0A71)
  126. $cat = self::OT_SM; /* GURMUKHI ADDAK. More like consonant medial. like 0A75. */
  127. if ($cat == self::OT_Repha) {
  128. /* There are two kinds of characters marked as Repha:
  129. * - The ones that are GenCat=Mn are already positioned visually, ie. after base. (eg. Khmer)
  130. * - The ones that are GenCat=Lo is encoded logically, ie. beginning of syllable. (eg. Malayalam)
  131. *
  132. * We recategorize the first kind to look like a Nukta and attached to the base directly.
  133. */
  134. if ($info['general_category'] == UCDN::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
  135. $cat = self::OT_N;
  136. }
  137. /*
  138. * Re-assign position.
  139. */
  140. if ((self::FLAG($cat) & (self::FLAG(self::OT_C) | self::FLAG(self::OT_CM) | self::FLAG(self::OT_Ra) | self::FLAG(self::OT_V) | self::FLAG(self::OT_NBSP) | self::FLAG(self::OT_DOTTEDCIRCLE)))) { // = CONSONANT_FLAGS like is_consonant
  141. if ($scriptblock == UCDN::SCRIPT_KHMER)
  142. $pos = self::POS_BELOW_C; /* Khmer differs from Indic here. */
  143. else
  144. $pos = self::POS_BASE_C; /* Will recategorize later based on font lookups. */
  145. if (self::is_ra($u))
  146. $cat = self::OT_Ra;
  147. }
  148. else if ($cat == self::OT_M) {
  149. $pos = self::matra_position($u, $pos);
  150. } else if ($cat == self::OT_SM || $cat == self::OT_VD) {
  151. $pos = self::POS_SMVD;
  152. }
  153. if ($u == 0x0B01)
  154. $pos = self::POS_BEFORE_SUB; /* Oriya Bindu is BeforeSub in the spec. */
  155. $info['indic_category'] = $cat;
  156. $info['indic_position'] = $pos;
  157. }
  158. // syllable_type
  159. const CONSONANT_SYLLABLE = 0;
  160. const VOWEL_SYLLABLE = 1;
  161. const STANDALONE_CLUSTER = 2;
  162. const BROKEN_CLUSTER = 3;
  163. const NON_INDIC_CLUSTER = 4;
  164. public static function set_syllables(&$o, $s, &$broken_syllables)
  165. {
  166. $ptr = 0;
  167. $syllable_serial = 1;
  168. $broken_syllables = false;
  169. while ($ptr < strlen($s)) {
  170. $match = '';
  171. $syllable_length = 1;
  172. $syllable_type = self::NON_INDIC_CLUSTER;
  173. // CONSONANT_SYLLABLE Consonant syllable
  174. // From OT spec:
  175. if (preg_match('/^([CR]m*[N]?(H[ZJ]?|[ZJ]H))*[CR]m*[N]?[A]?(H[ZJ]?|[M]*[N]?[H]?)?[S]?[v]{0,2}/', substr($s, $ptr), $ma)) {
  176. // From HarfBuzz:
  177. //if (preg_match('/^r?([CR]J?(Z?[N]{0,2})?[ZJ]?H(J[N]?)?){0,4}[CR]J?(Z?[N]{0,2})?A?((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
  178. $syllable_length = strlen($ma[0]);
  179. $syllable_type = self::CONSONANT_SYLLABLE;
  180. }
  181. // VOWEL_SYLLABLE Vowel-based syllable
  182. // From OT spec:
  183. else if (preg_match('/^(RH|r)?V[N]?([ZJ]?H[CR]m*|J[CR]m*)?([M]*[N]?[H]?)?[S]?[v]{0,2}/', substr($s, $ptr), $ma)) {
  184. // From HarfBuzz:
  185. //else if (preg_match('/^(RH|r)?V(Z?[N]{0,2})?(J|([ZJ]?H(J[N]?)?[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2})/', substr($s,$ptr), $ma)) {
  186. $syllable_length = strlen($ma[0]);
  187. $syllable_type = self::VOWEL_SYLLABLE;
  188. }
  189. /* Apply only if it's a word start. */
  190. // STANDALONE_CLUSTER Stand Alone syllable at start of word
  191. // From OT spec:
  192. else if (($ptr == 0 ||
  193. $o[$ptr - 1]['general_category'] < UCDN::UNICODE_GENERAL_CATEGORY_LOWERCASE_LETTER ||
  194. $o[$ptr - 1]['general_category'] > UCDN::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK
  195. ) && (preg_match('/^(RH|r)?[sD][N]?([ZJ]?H[CR]m*)?([M]*[N]?[H]?)?[S]?[v]{0,2}/', substr($s, $ptr), $ma))) {
  196. // From HarfBuzz:
  197. // && (preg_match('/^(RH|r)?[sD](Z?[N]{0,2})?(([ZJ]?H(J[N]?)?)[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})?(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
  198. $syllable_length = strlen($ma[0]);
  199. $syllable_type = self::STANDALONE_CLUSTER;
  200. }
  201. // BROKEN_CLUSTER syllable
  202. else if (preg_match('/^(RH|r)?[N]?([ZJ]?H[CR])?([M]*[N]?[H]?)?[S]?[v]{0,2}/', substr($s, $ptr), $ma)) {
  203. // From HarfBuzz:
  204. //else if (preg_match('/^(RH|r)?(Z?[N]{0,2})?(([ZJ]?H(J[N]?)?)[CR]J?(Z?[N]{0,2})?){0,4}((([ZJ]?H(J[N]?)?)|HZ)|(HJ)?([ZJ]{0,3}M[N]?(H|JHJR)?){0,4})(S[Z]?)?[v]{0,2}/', substr($s,$ptr), $ma)) {
  205. if (strlen($ma[0])) { // May match blank
  206. $syllable_length = strlen($ma[0]);
  207. $syllable_type = self::BROKEN_CLUSTER;
  208. $broken_syllables = true;
  209. }
  210. }
  211. for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
  212. $o[$i]['syllable'] = ($syllable_serial << 4) | $syllable_type;
  213. }
  214. $ptr += $syllable_length;
  215. $syllable_serial++;
  216. if ($syllable_serial == 16)
  217. $syllable_serial = 1;
  218. }
  219. }
  220. public static function set_syllables_sinhala(&$o, $s, &$broken_syllables)
  221. {
  222. $ptr = 0;
  223. $syllable_serial = 1;
  224. $broken_syllables = false;
  225. while ($ptr < strlen($s)) {
  226. $match = '';
  227. $syllable_length = 1;
  228. $syllable_type = self::NON_INDIC_CLUSTER;
  229. // CONSONANT_SYLLABLE Consonant syllable
  230. // From OT spec:
  231. if (preg_match('/^([CR]HJ|[CR]JH){0,8}[CR][HM]{0,3}[S]{0,1}/', substr($s, $ptr), $ma)) {
  232. $syllable_length = strlen($ma[0]);
  233. $syllable_type = self::CONSONANT_SYLLABLE;
  234. }
  235. // VOWEL_SYLLABLE Vowel-based syllable
  236. // From OT spec:
  237. else if (preg_match('/^V[S]{0,1}/', substr($s, $ptr), $ma)) {
  238. $syllable_length = strlen($ma[0]);
  239. $syllable_type = self::VOWEL_SYLLABLE;
  240. }
  241. for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
  242. $o[$i]['syllable'] = ($syllable_serial << 4) | $syllable_type;
  243. }
  244. $ptr += $syllable_length;
  245. $syllable_serial++;
  246. if ($syllable_serial == 16)
  247. $syllable_serial = 1;
  248. }
  249. }
  250. public static function set_syllables_khmer(&$o, $s, &$broken_syllables)
  251. {
  252. $ptr = 0;
  253. $syllable_serial = 1;
  254. $broken_syllables = false;
  255. while ($ptr < strlen($s)) {
  256. $match = '';
  257. $syllable_length = 1;
  258. $syllable_type = self::NON_INDIC_CLUSTER;
  259. // CONSONANT_SYLLABLE Consonant syllable
  260. if (preg_match('/^r?([CR]J?((Z?F)?[N]{0,2})?[ZJ]?G(JN?)?){0,4}[CR]J?((Z?F)?[N]{0,2})?A?((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})?(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2}/', substr($s, $ptr), $ma)) {
  261. $syllable_length = strlen($ma[0]);
  262. $syllable_type = self::CONSONANT_SYLLABLE;
  263. }
  264. // VOWEL_SYLLABLE Vowel-based syllable
  265. else if (preg_match('/^(RH|r)?V((Z?F)?[N]{0,2})?(J|([ZJ]?G(JN?)?[CR]J?((Z?F)?[N]{0,2})?){0,4}((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})?(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2})/', substr($s, $ptr), $ma)) {
  266. $syllable_length = strlen($ma[0]);
  267. $syllable_type = self::VOWEL_SYLLABLE;
  268. }
  269. // BROKEN_CLUSTER syllable
  270. else if (preg_match('/^(RH|r)?((Z?F)?[N]{0,2})?(([ZJ]?G(JN?)?)[CR]J?((Z?F)?[N]{0,2})?){0,4}((([ZJ]?G(JN?)?)|GZ)|(GJ)?([ZJ]{0,3}MN?(H|JHJR)?){0,4})(G([CR]J?((Z?F)?[N]{0,2})?|V))?(SZ?)?[v]{0,2}/', substr($s, $ptr), $ma)) {
  271. if (strlen($ma[0])) { // May match blank
  272. $syllable_length = strlen($ma[0]);
  273. $syllable_type = self::BROKEN_CLUSTER;
  274. $broken_syllables = true;
  275. }
  276. }
  277. for ($i = $ptr; $i < $ptr + $syllable_length; $i++) {
  278. $o[$i]['syllable'] = ($syllable_serial << 4) | $syllable_type;
  279. }
  280. $ptr += $syllable_length;
  281. $syllable_serial++;
  282. if ($syllable_serial == 16)
  283. $syllable_serial = 1;
  284. }
  285. }
  286. public static function initial_reordering(&$info, $GSUBdata, $broken_syllables, $indic_config, $scriptblock, $is_old_spec, $dottedcircle)
  287. {
  288. self::update_consonant_positions($info, $GSUBdata);
  289. if ($broken_syllables && $dottedcircle) {
  290. self::insert_dotted_circles($info, $dottedcircle);
  291. }
  292. $count = count($info);
  293. if (!$count)
  294. return;
  295. $last = 0;
  296. $last_syllable = $info[0]['syllable'];
  297. for ($i = 1; $i < $count; $i++) {
  298. if ($last_syllable != $info[$i]['syllable']) {
  299. self::initial_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $i);
  300. $last = $i;
  301. $last_syllable = $info[$last]['syllable'];
  302. }
  303. }
  304. self::initial_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $count);
  305. }
  306. public static function update_consonant_positions(&$info, $GSUBdata)
  307. {
  308. $count = count($info);
  309. for ($i = 0; $i < $count; $i++) {
  310. if ($info[$i]['indic_position'] == self::POS_BASE_C) {
  311. $c = $info[$i]['uni'];
  312. // If would substitute...
  313. if (isset($GSUBdata['pref'][$c])) {
  314. $info[$i]['indic_position'] = self::POS_POST_C;
  315. } else if (isset($GSUBdata['blwf'][$c])) {
  316. $info[$i]['indic_position'] = self::POS_BELOW_C;
  317. } else if (isset($GSUBdata['pstf'][$c])) {
  318. $info[$i]['indic_position'] = self::POS_POST_C;
  319. }
  320. }
  321. }
  322. }
  323. public static function insert_dotted_circles(&$info, $dottedcircle)
  324. {
  325. $idx = 0;
  326. $last_syllable = 0;
  327. while ($idx < count($info)) {
  328. $syllable = $info[$idx]['syllable'];
  329. $syllable_type = ($syllable & 0x0F);
  330. if ($last_syllable != $syllable && $syllable_type == self::BROKEN_CLUSTER) {
  331. $last_syllable = $syllable;
  332. $dottedcircle[0]['syllable'] = $info[$idx]['syllable'];
  333. /* Insert dottedcircle after possible Repha. */
  334. while ($idx < count($info) && $last_syllable == $info[$idx]['syllable'] && $info[$idx]['indic_category'] == self::OT_Repha)
  335. $idx++;
  336. array_splice($info, $idx, 0, $dottedcircle);
  337. } else {
  338. $idx++;
  339. }
  340. }
  341. // I am not sue how this code below got in here, since $idx should now be > count($info) and thus invalid.
  342. // In case I am missing something(!) I'll leave a warning here for now:
  343. if (isset($info[$idx])) {
  344. throw new MpdfException('Unexpected error occured in Indic processing');
  345. }
  346. // In case of final bloken cluster...
  347. //$syllable = $info[$idx]['syllable'];
  348. //$syllable_type = ($syllable & 0x0F);
  349. //if ($last_syllable != $syllable && $syllable_type == self::BROKEN_CLUSTER) {
  350. // $dottedcircle[0]['syllable'] = $info[$idx]['syllable'];
  351. // array_splice($info, $idx, 0, $dottedcircle);
  352. //}
  353. }
  354. /* Rules from:
  355. * https://www.microsoft.com/typography/otfntdev/devanot/shaping.aspx */
  356. public static function initial_reordering_syllable(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $start, $end)
  357. {
  358. /* vowel_syllable: We made the vowels look like consonants. So uses the consonant logic! */
  359. /* broken_cluster: We already inserted dotted-circles, so just call the standalone_cluster. */
  360. /* standalone_cluster: We treat NBSP/dotted-circle as if they are consonants, so we should just chain. */
  361. $syllable_type = ($info[$start]['syllable'] & 0x0F);
  362. if ($syllable_type == self::NON_INDIC_CLUSTER) {
  363. return;
  364. }
  365. if ($syllable_type == self::BROKEN_CLUSTER || $syllable_type == self::STANDALONE_CLUSTER) {
  366. //if ($uniscribe_bug_compatible) {
  367. /* For dotted-circle, this is what Uniscribe does:
  368. * If dotted-circle is the last glyph, it just does nothing.
  369. * i.e. It doesn't form Reph. */
  370. if ($info[$end - 1]['indic_category'] == self::OT_DOTTEDCIRCLE) {
  371. return;
  372. }
  373. }
  374. /* 1. Find base consonant:
  375. *
  376. * The shaping engine finds the base consonant of the syllable, using the
  377. * following algorithm: starting from the end of the syllable, move backwards
  378. * until a consonant is found that does not have a below-base or post-base
  379. * form (post-base forms have to follow below-base forms), or that is not a
  380. * pre-base reordering Ra, or arrive at the first consonant. The consonant
  381. * stopped at will be the base.
  382. *
  383. * o If the syllable starts with Ra + Halant (in a script that has Reph)
  384. * and has more than one consonant, Ra is excluded from candidates for
  385. * base consonants.
  386. */
  387. $base = $end;
  388. $has_reph = false;
  389. $limit = $start;
  390. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  391. /* -> If the syllable starts with Ra + Halant (in a script that has Reph)
  392. * and has more than one consonant, Ra is excluded from candidates for
  393. * base consonants. */
  394. if (count($GSUBdata['rphf']) /* ?? $indic_plan->mask_array[RPHF] */ && $start + 3 <= $end &&
  395. (
  396. ($indic_config[4] == self::REPH_MODE_IMPLICIT && !self::is_joiner($info[$start + 2])) ||
  397. ($indic_config[4] == self::REPH_MODE_EXPLICIT && $info[$start + 2]['indic_category'] == self::OT_ZWJ)
  398. )) {
  399. /* See if it matches the 'rphf' feature. */
  400. //$glyphs = array($info[$start]['uni'], $info[$start + 1]['uni']);
  401. //if ($indic_plan->rphf->would_substitute ($glyphs, count($glyphs), true, face)) {
  402. if (isset($GSUBdata['rphf'][$info[$start]['uni']]) && self::is_halant_or_coeng($info[$start + 1])) {
  403. $limit += 2;
  404. while ($limit < $end && self::is_joiner($info[$limit]))
  405. $limit++;
  406. $base = $start;
  407. $has_reph = true;
  408. }
  409. } else if ($indic_config[4] == self::REPH_MODE_LOG_REPHA && $info[$start]['indic_category'] == self::OT_Repha) {
  410. $limit += 1;
  411. while ($limit < $end && self::is_joiner($info[$limit]))
  412. $limit++;
  413. $base = $start;
  414. $has_reph = true;
  415. }
  416. }
  417. switch ($indic_config[2]) { // base_pos
  418. case self::BASE_POS_LAST:
  419. /* -> starting from the end of the syllable, move backwards */
  420. $i = $end;
  421. $seen_below = false;
  422. do {
  423. $i--;
  424. /* -> until a consonant is found */
  425. if (self::is_consonant($info[$i])) {
  426. /* -> that does not have a below-base or post-base form
  427. * (post-base forms have to follow below-base forms), */
  428. if ($info[$i]['indic_position'] != self::POS_BELOW_C && ($info[$i]['indic_position'] != self::POS_POST_C || $seen_below)) {
  429. $base = $i;
  430. break;
  431. }
  432. if ($info[$i]['indic_position'] == self::POS_BELOW_C)
  433. $seen_below = true;
  434. /* -> or that is not a pre-base reordering Ra,
  435. *
  436. * IMPLEMENTATION NOTES:
  437. *
  438. * Our pre-base reordering Ra's are marked POS_POST_C, so will be skipped
  439. * by the logic above already.
  440. */
  441. /* -> or arrive at the first consonant. The consonant stopped at will
  442. * be the base. */
  443. $base = $i;
  444. }
  445. else {
  446. /* A ZWJ after a Halant stops the base search, and requests an explicit
  447. * half form.
  448. * [A ZWJ before a Halant, requests a subjoined form instead, and hence
  449. * search continues. This is particularly important for Bengali
  450. * sequence Ra,H,Ya that should form Ya-Phalaa by subjoining Ya] */
  451. if ($start < $i && $info[$i]['indic_category'] == self::OT_ZWJ && $info[$i - 1]['indic_category'] == self::OT_H) {
  452. if (!defined("OMIT_INDIC_FIX_1") || OMIT_INDIC_FIX_1 != 1) {
  453. $base = $i;
  454. } // INDIC_FIX_1
  455. break;
  456. }
  457. // ZKI8
  458. if ($start < $i && $info[$i]['indic_category'] == self::OT_ZWNJ) {
  459. break;
  460. }
  461. }
  462. } while ($i > $limit);
  463. break;
  464. case self::BASE_POS_FIRST:
  465. /* In scripts without half forms (eg. Khmer), the first consonant is always the base. */
  466. if (!$has_reph)
  467. $base = $limit;
  468. /* Find the last base consonant that is not blocked by ZWJ. If there is
  469. * a ZWJ right before a base consonant, that would request a subjoined form. */
  470. for ($i = $limit; $i < $end; $i++) {
  471. if (self::is_consonant($info[$i]) && $info[$i]['indic_position'] == self::POS_BASE_C) {
  472. if ($limit < $i && $info[$i - 1]['indic_category'] == self::OT_ZWJ)
  473. break;
  474. else
  475. $base = $i;
  476. }
  477. }
  478. /* Mark all subsequent consonants as below. */
  479. for ($i = $base + 1; $i < $end; $i++) {
  480. if (self::is_consonant($info[$i]) && $info[$i]['indic_position'] == self::POS_BASE_C)
  481. $info[$i]['indic_position'] = self::POS_BELOW_C;
  482. }
  483. break;
  484. //default:
  485. //assert (false);
  486. /* fallthrough */
  487. }
  488. /* -> If the syllable starts with Ra + Halant (in a script that has Reph)
  489. * and has more than one consonant, Ra is excluded from candidates for
  490. * base consonants.
  491. *
  492. * Only do this for unforced Reph. (ie. not for Ra,H,ZWJ. */
  493. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  494. if ($has_reph && $base == $start && $limit - $base <= 2) {
  495. /* Have no other consonant, so Reph is not formed and Ra becomes base. */
  496. $has_reph = false;
  497. }
  498. }
  499. /* 2. Decompose and reorder Matras:
  500. *
  501. * Each matra and any syllable modifier sign in the cluster are moved to the
  502. * appropriate position relative to the consonant(s) in the cluster. The
  503. * shaping engine decomposes two- or three-part matras into their constituent
  504. * parts before any repositioning. Matra characters are classified by which
  505. * consonant in a conjunct they have affinity for and are reordered to the
  506. * following positions:
  507. *
  508. * o Before first half form in the syllable
  509. * o After subjoined consonants
  510. * o After post-form consonant
  511. * o After main consonant (for above marks)
  512. *
  513. * IMPLEMENTATION NOTES:
  514. *
  515. * The normalize() routine has already decomposed matras for us, so we don't
  516. * need to worry about that.
  517. */
  518. /* 3. Reorder marks to canonical order:
  519. *
  520. * Adjacent nukta and halant or nukta and vedic sign are always repositioned
  521. * if necessary, so that the nukta is first.
  522. *
  523. * IMPLEMENTATION NOTES:
  524. *
  525. * Use the combining Class from Unicode categories? to bubble_sort.
  526. */
  527. /* Reorder characters */
  528. for ($i = $start; $i < $base; $i++)
  529. $info[$i]['indic_position'] = min(self::POS_PRE_C, $info[$i]['indic_position']);
  530. if ($base < $end)
  531. $info[$base]['indic_position'] = self::POS_BASE_C;
  532. /* Mark final consonants. A final consonant is one appearing after a matra,
  533. * ? only in Khmer. */
  534. for ($i = $base + 1; $i < $end; $i++)
  535. if ($info[$i]['indic_category'] == self::OT_M) {
  536. for ($j = $i + 1; $j < $end; $j++)
  537. if (self::is_consonant($info[$j])) {
  538. $info[$j]['indic_position'] = self::POS_FINAL_C;
  539. break;
  540. }
  541. break;
  542. }
  543. /* Handle beginning Ra */
  544. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  545. if ($has_reph)
  546. $info[$start]['indic_position'] = self::POS_RA_TO_BECOME_REPH;
  547. }
  548. /* For old-style Indic script tags, move the first post-base Halant after
  549. * last consonant. Only do this if there is *not* a Halant after last
  550. * consonant. Otherwise it becomes messy. */
  551. if ($is_old_spec) {
  552. for ($i = $base + 1; $i < $end; $i++) {
  553. if ($info[$i]['indic_category'] == self::OT_H) {
  554. for ($j = $end - 1; $j > $i; $j--) {
  555. if (self::is_consonant($info[$j]) || $info[$j]['indic_category'] == self::OT_H) {
  556. break;
  557. }
  558. }
  559. if ($info[$j]['indic_category'] != self::OT_H && $j > $i) {
  560. /* Move Halant to after last consonant. */
  561. self::_move_info_pos($info, $i, $j + 1);
  562. }
  563. break;
  564. }
  565. }
  566. }
  567. /* Attach misc marks to previous char to move with them. */
  568. $last_pos = self::POS_START;
  569. for ($i = $start; $i < $end; $i++) {
  570. if ((self::FLAG($info[$i]['indic_category']) & (self::FLAG(self::OT_ZWJ) | self::FLAG(self::OT_ZWNJ) | self::FLAG(self::OT_N) | self::FLAG(self::OT_RS) | self::FLAG(self::OT_H) | self::FLAG(self::OT_Coeng) ))) {
  571. $info[$i]['indic_position'] = $last_pos;
  572. if ($info[$i]['indic_category'] == self::OT_H && $info[$i]['indic_position'] == self::POS_PRE_M) {
  573. /*
  574. * Uniscribe doesn't move the Halant with Left Matra.
  575. * TEST: U+092B,U+093F,U+094DE
  576. * We follow. This is important for the Sinhala
  577. * U+0DDA split matra since it decomposes to U+0DD9,U+0DCA
  578. * where U+0DD9 is a left matra and U+0DCA is the virama.
  579. * We don't want to move the virama with the left matra.
  580. * TEST: U+0D9A,U+0DDA
  581. */
  582. for ($j = $i; $j > $start; $j--)
  583. if ($info[$j - 1]['indic_position'] != self::POS_PRE_M) {
  584. $info[$i]['indic_position'] = $info[$j - 1]['indic_position'];
  585. break;
  586. }
  587. }
  588. } else if ($info[$i]['indic_position'] != self::POS_SMVD) {
  589. $last_pos = $info[$i]['indic_position'];
  590. }
  591. }
  592. /* Re-attach ZWJ, ZWNJ, and halant to next char, for after-base consonants. */
  593. $last_halant = $end;
  594. for ($i = $base + 1; $i < $end; $i++) {
  595. if (self::is_halant_or_coeng($info[$i]))
  596. $last_halant = $i;
  597. else if (self::is_consonant($info[$i])) {
  598. for ($j = $last_halant; $j < $i; $j++)
  599. if ($info[$j]['indic_position'] != self::POS_SMVD)
  600. $info[$j]['indic_position'] = $info[$i]['indic_position'];
  601. }
  602. }
  603. if ($scriptblock == UCDN::SCRIPT_KHMER) {
  604. /* KHMER_FIX_2 */
  605. /* Move Coeng+RO (Halant,Ra) sequence before base consonant. */
  606. for ($i = $base + 1; $i < $end; $i++) {
  607. if (self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni'])) {
  608. $info[$i]['indic_position'] = self::POS_PRE_C;
  609. $info[$i + 1]['indic_position'] = self::POS_PRE_C;
  610. break;
  611. }
  612. }
  613. }
  614. /*
  615. if (!defined("OMIT_INDIC_FIX_2") || OMIT_INDIC_FIX_2 != 1) {
  616. // INDIC_FIX_2
  617. $ZWNJ_found = false;
  618. $POST_ZWNJ_c_found = false;
  619. for ($i = $base + 1; $i < $end; $i++) {
  620. if ($info[$i]['indic_category'] == self::OT_ZWNJ) { $ZWNJ_found = true; }
  621. else if ($ZWNJ_found && $info[$i]['indic_category'] == self::OT_C) { $POST_ZWNJ_c_found = true; }
  622. else if ($POST_ZWNJ_c_found && $info[$i]['indic_position'] == self::POS_BEFORE_SUB) { $info[$i]['indic_position'] = self::POS_AFTER_SUB; }
  623. }
  624. }
  625. */
  626. /* Setup masks now */
  627. for ($i = $start; $i < $end; $i++) {
  628. $info[$i]['mask'] = 0;
  629. }
  630. if ($scriptblock == UCDN::SCRIPT_KHMER) {
  631. /* Find a Coeng+RO (Halant,Ra) sequence and mark it for pre-base processing. */
  632. $mask = self::FLAG(self::PREF);
  633. for ($i = $base; $i < $end - 1; $i++) { /* KHMER_FIX_1 From $start (not base) */
  634. if (self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni'])) {
  635. $info[$i]['mask'] |= self::FLAG(self::PREF);
  636. $info[$i + 1]['mask'] |= self::FLAG(self::PREF);
  637. /* Mark the subsequent stuff with 'cfar'. Used in Khmer.
  638. * Read the feature spec.
  639. * This allows distinguishing the following cases with MS Khmer fonts:
  640. * U+1784,U+17D2,U+179A,U+17D2,U+1782 [C+Coeng+RO+Coeng+C] => Should activate CFAR
  641. * U+1784,U+17D2,U+1782,U+17D2,U+179A [C+Coeng+C+Coeng+RO] => Should NOT activate CFAR
  642. */
  643. for ($j = ($i + 2); $j < $end; $j++)
  644. $info[$j]['mask'] |= self::FLAG(self::CFAR);
  645. break;
  646. }
  647. }
  648. }
  649. /* Sit tight, rock 'n roll! */
  650. self::bubble_sort($info, $start, $end - $start);
  651. /* Find base again */
  652. $base = $end;
  653. for ($i = $start; $i < $end; $i++) {
  654. if ($info[$i]['indic_position'] == self::POS_BASE_C) {
  655. $base = $i;
  656. break;
  657. }
  658. }
  659. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  660. /* Reph */
  661. for ($i = $start; $i < $end; $i++) {
  662. if ($info[$i]['indic_position'] == self::POS_RA_TO_BECOME_REPH) {
  663. $info[$i]['mask'] |= self::FLAG(self::RPHF);
  664. }
  665. }
  666. /* Pre-base */
  667. $mask = self::FLAG(self::HALF);
  668. for ($i = $start; $i < $base; $i++) {
  669. $info[$i]['mask'] |= $mask;
  670. }
  671. }
  672. /* Post-base */
  673. $mask = (self::FLAG(self::BLWF) | self::FLAG(self::ABVF) | self::FLAG(self::PSTF));
  674. for ($i = $base + 1; $i < $end; $i++) {
  675. $info[$i]['mask'] |= $mask;
  676. }
  677. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  678. if (!defined("OMIT_INDIC_FIX_3") || OMIT_INDIC_FIX_3 != 1) {
  679. /* INDIC_FIX_3 */
  680. /* Find a (pre-base) Consonant, Halant,Ra sequence and mark Halant|Ra for below-base BLWF processing. */
  681. // TEST CASE &#x995;&#x9cd;&#x9b0;&#x9cd;&#x995; in FreeSans versus Vrinda
  682. if (($base - $start) >= 3) {
  683. for ($i = $start; $i < ($base - 2); $i++) {
  684. if (self::is_consonant($info[$i])) {
  685. if (self::is_halant_or_coeng($info[$i + 1]) && self::is_ra($info[$i + 2]['uni'])) {
  686. // If would substitute Halant+Ra...BLWF
  687. if (isset($GSUBdata['blwf'][$info[$i + 2]['uni']])) {
  688. $info[$i + 1]['mask'] |= self::FLAG(self::BLWF);
  689. $info[$i + 2]['mask'] |= self::FLAG(self::BLWF);
  690. }
  691. /* If would not substitute as blwf, mark Ra+Halant for RPHF using following Halant (if present) */ else if (self::is_halant_or_coeng($info[$i + 3])) {
  692. $info[$i + 2]['mask'] |= self::FLAG(self::RPHF);
  693. $info[$i + 3]['mask'] |= self::FLAG(self::RPHF);
  694. }
  695. break;
  696. }
  697. }
  698. }
  699. }
  700. }
  701. }
  702. if ($is_old_spec && $scriptblock == UCDN::SCRIPT_DEVANAGARI) {
  703. /* Old-spec eye-lash Ra needs special handling. From the spec:
  704. * "The feature 'below-base form' is applied to consonants
  705. * having below-base forms and following the base consonant.
  706. * The exception is vattu, which may appear below half forms
  707. * as well as below the base glyph. The feature 'below-base
  708. * form' will be applied to all such occurrences of Ra as well."
  709. *
  710. * Test case: U+0924,U+094D,U+0930,U+094d,U+0915
  711. * with Sanskrit 2003 font.
  712. *
  713. * However, note that Ra,Halant,ZWJ is the correct way to
  714. * request eyelash form of Ra, so we wouldbn't inhibit it
  715. * in that sequence.
  716. *
  717. * Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915
  718. */
  719. for ($i = $start; ($i + 1) < $base; $i++) {
  720. if ($info[$i]['indic_category'] == self::OT_Ra && $info[$i + 1]['indic_category'] == self::OT_H &&
  721. ($i + 2 == $base || $info[$i + 2]['indic_category'] != self::OT_ZWJ)) {
  722. $info[$i]['mask'] |= self::FLAG(self::BLWF);
  723. $info[$i + 1]['mask'] |= self::FLAG(self::BLWF);
  724. }
  725. }
  726. }
  727. if ($scriptblock != UCDN::SCRIPT_KHMER) {
  728. if (count($GSUBdata['pref']) && $base + 2 < $end) {
  729. /* Find a Halant,Ra sequence and mark it for pre-base processing. */
  730. for ($i = $base + 1; $i + 1 < $end; $i++) {
  731. // If old_spec find Ra-Halant...
  732. if ((isset($GSUBdata['pref'][$info[$i + 1]['uni']]) && self::is_halant_or_coeng($info[$i]) && self::is_ra($info[$i + 1]['uni']) ) ||
  733. ($is_old_spec && isset($GSUBdata['pref'][$info[$i]['uni']]) && self::is_halant_or_coeng($info[$i + 1]) && self::is_ra($info[$i]['uni']) )
  734. ) {
  735. $info[$i++]['mask'] |= self::FLAG(self::PREF);
  736. $info[$i++]['mask'] |= self::FLAG(self::PREF);
  737. break;
  738. }
  739. }
  740. }
  741. }
  742. /* Apply ZWJ/ZWNJ effects */
  743. for ($i = $start + 1; $i < $end; $i++) {
  744. if (self::is_joiner($info[$i])) {
  745. $non_joiner = ($info[$i]['indic_category'] == self::OT_ZWNJ);
  746. $j = $i;
  747. while ($j > $start) {
  748. if (defined("OMIT_INDIC_FIX_4") && OMIT_INDIC_FIX_4 == 1) {
  749. // INDIC_FIX_4 = do nothing - carry on //
  750. // ZWNJ should block H C from forming blwf post-base - need to unmask backwards beyond first consonant arrived at //
  751. if (!self::is_consonant($info[$j])) {
  752. break;
  753. }
  754. }
  755. $j--;
  756. /* ZWJ/ZWNJ should disable CJCT. They do that by simply
  757. * being there, since we don't skip them for the CJCT
  758. * feature (ie. F_MANUAL_ZWJ) */
  759. /* A ZWNJ disables HALF. */
  760. if ($non_joiner) {
  761. $info[$j]['mask'] &= ~(self::FLAG(self::HALF) | self::FLAG(self::BLWF));
  762. }
  763. }
  764. }
  765. }
  766. }
  767. public static function final_reordering(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec)
  768. {
  769. $count = count($info);
  770. if (!$count)
  771. return;
  772. $last = 0;
  773. $last_syllable = $info[0]['syllable'];
  774. for ($i = 1; $i < $count; $i++) {
  775. if ($last_syllable != $info[$i]['syllable']) {
  776. self::final_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $i);
  777. $last = $i;
  778. $last_syllable = $info[$last]['syllable'];
  779. }
  780. }
  781. self::final_reordering_syllable($info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $last, $count);
  782. }
  783. public static function final_reordering_syllable(&$info, $GSUBdata, $indic_config, $scriptblock, $is_old_spec, $start, $end)
  784. {
  785. /* 4. Final reordering:
  786. *
  787. * After the localized forms and basic shaping forms GSUB features have been
  788. * applied (see below), the shaping engine performs some final glyph
  789. * reordering before applying all the remaining font features to the entire
  790. * cluster.
  791. */
  792. /* Find base again */
  793. for ($base = $start; $base < $end; $base++)
  794. if ($info[$base]['indic_position'] >= self::POS_BASE_C) {
  795. if ($start < $base && $info[$base]['indic_position'] > self::POS_BASE_C)
  796. $base--;
  797. break;
  798. }
  799. if ($base == $end && $start < $base && $info[$base - 1]['indic_category'] != self::OT_ZWJ)
  800. $base--;
  801. while ($start < $base && isset($info[$base]) && ($info[$base]['indic_category'] == self::OT_H || $info[$base]['indic_category'] == self::OT_N))
  802. $base--;
  803. /* o Reorder matras:
  804. *
  805. * If a pre-base matra character had been reordered before applying basic
  806. * features, the glyph can be moved closer to the main consonant based on
  807. * whether half-forms had been formed. Actual position for the matra is
  808. * defined as "after last standalone halant glyph, after initial matra
  809. * position and before the main consonant". If ZWJ or ZWNJ follow this
  810. * halant, position is moved after it.
  811. */
  812. if ($start + 1 < $end && $start < $base) { /* Otherwise there can't be any pre-base matra characters. */
  813. /* If we lost track of base, alas, position before last thingy. */
  814. $new_pos = ($base == $end) ? $base - 2 : $base - 1;
  815. /* Malayalam / Tamil do not have "half" forms or explicit virama forms.
  816. * The glyphs formed by 'half' are Chillus or ligated explicit viramas.
  817. * We want to position matra after them.
  818. */
  819. if ($scriptblock != UCDN::SCRIPT_MALAYALAM && $scriptblock != UCDN::SCRIPT_TAMIL) {
  820. while ($new_pos > $start && !(self::is_one_of($info[$new_pos], (self::FLAG(self::OT_M) | self::FLAG(self::OT_H) | self::FLAG(self::OT_Coeng)))))
  821. $new_pos--;
  822. /* If we found no Halant we are done.
  823. * Otherwise only proceed if the Halant does
  824. * not belong to the Matra itself! */
  825. if (self::is_halant_or_coeng($info[$new_pos]) && $info[$new_pos]['indic_position'] != self::POS_PRE_M) {
  826. /* -> If ZWJ or ZWNJ follow this halant, position is moved after it. */
  827. if ($new_pos + 1 < $end && self::is_joiner($info[$new_pos + 1]))
  828. $new_pos++;
  829. } else
  830. $new_pos = $start; /* No move. */
  831. }
  832. if ($start < $new_pos && $info[$new_pos]['indic_position'] != self::POS_PRE_M) {
  833. /* Now go see if there's actually any matras... */
  834. for ($i = $new_pos; $i > $start; $i--)
  835. if ($info[$i - 1]['indic_position'] == self::POS_PRE_M) {
  836. $old_pos = $i - 1;
  837. //memmove (&info[$old_pos], &info[$old_pos + 1], ($new_pos - $old_pos) * sizeof ($info[0]));
  838. self::_move_info_pos($info, $old_pos, $new_pos + 1);
  839. if ($old_pos < $base && $base <= $new_pos) /* Shouldn't actually happen. */
  840. $base--;
  841. $new_pos--;
  842. }
  843. }
  844. }
  845. /* o Reorder reph:
  846. *
  847. * Reph's original position is always at the beginning of the syllable,
  848. * (i.e. it is not reordered at the character reordering stage). However,
  849. * it will be reordered according to the basic-forms shaping results.
  850. * Possible positions for reph, depending on the script, are; after main,
  851. * before post-base consonant forms, and after post-base consonant forms.
  852. */
  853. /* If there's anything after the Ra that has the REPH pos, it ought to be halant.
  854. * Which means that the font has failed to ligate the Reph. In which case, we
  855. * shouldn't move. */
  856. if ($start + 1 < $end &&
  857. $info[$start]['indic_position'] == self::POS_RA_TO_BECOME_REPH && $info[$start + 1]['indic_position'] != self::POS_RA_TO_BECOME_REPH) {
  858. $reph_pos = $indic_config[3];
  859. $skip_to_reph_step_5 = false;
  860. $skip_to_reph_move = false;
  861. /* 1. If reph should be positioned after post-base consonant forms,
  862. * proceed to step 5.
  863. */
  864. if ($reph_pos == self::REPH_POS_AFTER_POST) {
  865. $skip_to_reph_step_5 = true;
  866. }
  867. /* 2. If the reph repositioning class is not after post-base: target
  868. * position is after the first explicit halant glyph between the
  869. * first post-reph consonant and last main consonant. If ZWJ or ZWNJ
  870. * are following this halant, position is moved after it. If such
  871. * position is found, this is the target position. Otherwise,
  872. * proceed to the next step.
  873. *
  874. * Note: in old-implementation fonts, where classifications were
  875. * fixed in shaping engine, there was no case where reph position
  876. * will be found on this step.
  877. */
  878. if (!$skip_to_reph_step_5) {
  879. $new_reph_pos = $start + 1;
  880. while ($new_reph_pos < $base && !self::is_halant_or_coeng($info[$new_reph_pos]))
  881. $new_reph_pos++;
  882. if ($new_reph_pos < $base && self::is_halant_or_coeng($info[$new_reph_pos])) {
  883. /* ->If ZWJ or ZWNJ are following this halant, position is moved after it. */
  884. if ($new_reph_pos + 1 < $base && self::is_joiner($info[$new_reph_pos + 1]))
  885. $new_reph_pos++;
  886. $skip_to_reph_move = true;
  887. }
  888. }
  889. /* 3. If reph should be repositioned after the main consonant: find the
  890. * first consonant not ligated with main, or find the first
  891. * consonant that is not a potential pre-base reordering Ra.
  892. */
  893. if ($reph_pos == self::REPH_POS_AFTER_MAIN && !$skip_to_reph_move && !$skip_to_reph_step_5) {
  894. $new_reph_pos = $base;
  895. /* XXX Skip potential pre-base reordering Ra. */
  896. while ($new_reph_pos + 1 < $end && $info[$new_reph_pos + 1]['indic_position'] <= self::POS_AFTER_MAIN)
  897. $new_reph_pos++;
  898. if ($new_reph_pos < $end)
  899. $skip_to_reph_move = true;
  900. }
  901. /* 4. If reph should be positioned before post-base consonant, find
  902. * first post-base classified consonant not ligated with main. If no
  903. * consonant is found, the target position should be before the
  904. * first matra, syllable modifier sign or vedic sign.
  905. */
  906. /* This is our take on what step 4 is trying to say (and failing, BADLY). */
  907. if ($reph_pos == self::REPH_POS_AFTER_SUB && !$skip_to_reph_move && !$skip_to_reph_step_5) {
  908. $new_reph_pos = $base;
  909. while ($new_reph_pos < $end && isset($info[$new_reph_pos + 1]['indic_position']) &&
  910. !( self::FLAG($info[$new_reph_pos + 1]['indic_position']) & (self::FLAG(self::POS_POST_C) | self::FLAG(self::POS_AFTER_POST) | self::FLAG(self::POS_SMVD)))) {
  911. $new_reph_pos++;
  912. }
  913. if ($new_reph_pos < $end) {
  914. $skip_to_reph_move = true;
  915. }
  916. }
  917. /* 5. If no consonant is found in steps 3 or 4, move reph to a position
  918. * immediately before the first post-base matra, syllable modifier
  919. * sign or vedic sign that has a reordering class after the intended
  920. * reph position. For example, if the reordering position for reph
  921. * is post-main, it will skip above-base matras that also have a
  922. * post-main position.
  923. */
  924. if (!$skip_to_reph_move) {
  925. /* Copied from step 2. */
  926. $new_reph_pos = $start + 1;
  927. while ($new_reph_pos < $base && !self::is_halant_or_coeng($info[$new_reph_pos]))
  928. $new_reph_pos++;
  929. if ($new_reph_pos < $base && self::is_halant_or_coeng($info[$new_reph_pos])) {
  930. /* ->If ZWJ or ZWNJ are following this halant, position is moved after it. */
  931. if ($new_reph_pos + 1 < $base && self::is_joiner($info[$new_reph_pos + 1]))
  932. $new_reph_pos++;
  933. $skip_to_reph_move = true;
  934. }
  935. }
  936. /* 6. Otherwise, reorder reph to the end of the syllable.
  937. */
  938. if (!$skip_to_reph_move) {
  939. $new_reph_pos = $end - 1;
  940. while ($new_reph_pos > $start && $info[$new_reph_pos]['indic_position'] == self::POS_SMVD)
  941. $new_reph_pos--;
  942. /*
  943. * If the Reph is to be ending up after a Matra,Halant sequence,
  944. * position it before that Halant so it can interact with the Matra.
  945. * However, if it's a plain Consonant,Halant we shouldn't do that.
  946. * Uniscribe doesn't do this.
  947. * TEST: U+0930,U+094D,U+0915,U+094B,U+094D
  948. */
  949. //if (!$hb_options.uniscribe_bug_compatible && self::is_halant_or_coeng($info[$new_reph_pos])) {
  950. if (self::is_halant_or_coeng($info[$new_reph_pos])) {
  951. for ($i = $base + 1; $i < $new_reph_pos; $i++)
  952. if ($info[$i]['indic_category'] == self::OT_M) {
  953. /* Ok, got it. */
  954. $new_reph_pos--;
  955. }
  956. }
  957. }
  958. /* Move */
  959. self::_move_info_pos($info, $start, $new_reph_pos + 1);
  960. if ($start < $base && $base <= $new_reph_pos) {
  961. $base--;
  962. }
  963. }
  964. /* o Reorder pre-base reordering consonants:
  965. *
  966. * If a pre-base reordering consonant is found, reorder it according to
  967. * the following rules:
  968. */
  969. if (count($GSUBdata['pref']) && $base + 1 < $end) { /* Otherwise there can't be any pre-base reordering Ra. */
  970. for ($i = $base + 1; $i < $end; $i++) {
  971. if ($info[$i]['mask'] & self::FLAG(self::PREF)) {
  972. /* 1. Only reorder a glyph produced by substitution during application
  973. * of the <pref> feature. (Note that a font may shape a Ra consonant with
  974. * the feature generally but block it in certain contexts.)
  975. */
  976. // ??? Need to TEST if actual substitution has occurred
  977. if ($i + 1 == $end || ($info[$i + 1]['mask'] & self::FLAG(self::PREF)) == 0) {
  978. /*
  979. * 2. Try to find a target position the same way as for pre-base matra.
  980. * If it is found, reorder pre-base consonant glyph.
  981. *
  982. * 3. If position is not found, reorder immediately before main
  983. * consonant.
  984. */
  985. $new_pos = $base;
  986. /* Malayalam / Tamil do not have "half" forms or explicit virama forms.
  987. * The glyphs formed by 'half' are Chillus or ligated explicit viramas.
  988. * We want to position matra after them.
  989. */
  990. if ($scriptblock != UCDN::SCRIPT_MALAYALAM && $scriptblock != UCDN::SCRIPT_TAMIL) {
  991. while ($new_pos > $start &&
  992. !(self::is_one_of($info[$new_pos - 1], self::FLAG(self::OT_M) | self::FLAG(self::OT_H) | self::FLAG(self::OT_Coeng))))
  993. $new_pos--;
  994. /* In Khmer coeng model, a V,Ra can go *after* matras. If it goes after a
  995. * split matra, it should be reordered to *before* the left part of such matra. */
  996. if ($new_pos > $start && $info[$new_pos - 1]['indic_category'] == self::OT_M) {
  997. $old_pos = i;
  998. for ($i = $base + 1; $i < $old_pos; $i++)
  999. if ($info[$i]['indic_category'] == self::OT_M) {
  1000. $new_pos--;
  1001. break;
  1002. }
  1003. }
  1004. }
  1005. if ($new_pos > $start && self::is_halant_or_coeng($info[$new_pos - 1])) {
  1006. /* -> If ZWJ or ZWNJ follow this halant, position is moved after it. */
  1007. if ($new_pos < $end && self::is_joiner($info[$new_pos]))
  1008. $new_pos++;
  1009. }
  1010. $old_pos = $i;
  1011. self::_move_info_pos($info, $old_pos, $new_pos);
  1012. if ($new_pos <= $base && $base < $old_pos)
  1013. $base++;
  1014. }
  1015. break;
  1016. }
  1017. }
  1018. }
  1019. /* Apply 'init' to the Left Matra if it's a word start. */
  1020. if ($info[$start]['indic_position'] == self::POS_PRE_M &&
  1021. ($start == 0 ||
  1022. ($info[$start - 1]['general_category'] < UCDN::UNICODE_GENERAL_CATEGORY_FORMAT || $info[$start - 1]['general_category'] > UCDN::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK)
  1023. )) {
  1024. $info[$start]['mask'] |= self::FLAG(self::INIT);
  1025. }
  1026. /*
  1027. * Finish off and go home!
  1028. */
  1029. }
  1030. public static function _move_info_pos(&$info, $from, $to)
  1031. {
  1032. $t = array();
  1033. $t[0] = $info[$from];
  1034. if ($from > $to) {
  1035. array_splice($info, $from, 1);
  1036. array_splice($info, $to, 0, $t);
  1037. } else {
  1038. array_splice($info, $to, 0, $t);
  1039. array_splice($info, $from, 1);
  1040. }
  1041. }
  1042. public static $ra_chars = array(
  1043. 0x0930 => 1, /* Devanagari */
  1044. 0x09B0 => 1, /* Bengali */
  1045. 0x09F0 => 1, /* Bengali (Assamese) */
  1046. 0x0A30 => 1, /* Gurmukhi */ /* No Reph */
  1047. 0x0AB0 => 1, /* Gujarati */
  1048. 0x0B30 => 1, /* Oriya */
  1049. 0x0BB0 => 1, /* Tamil */ /* No Reph */
  1050. 0x0C30 => 1, /* Telugu */ /* Reph formed only with ZWJ */
  1051. 0x0CB0 => 1, /* Kannada */
  1052. 0x0D30 => 1, /* Malayalam */ /* No Reph, Logical Repha */
  1053. 0x0DBB => 1, /* Sinhala */ /* Reph formed only with ZWJ */
  1054. 0x179A => 1, /* Khmer */ /* No Reph, Visual Repha */
  1055. );
  1056. public static function is_ra($u)
  1057. {
  1058. if (isset(self::$ra_chars[$u]))
  1059. return true;
  1060. return false;
  1061. }
  1062. public static function is_one_of($info, $flags)
  1063. {
  1064. if (isset($info['is_ligature']) && $info['is_ligature'])
  1065. return false; /* If it ligated, all bets are off. */
  1066. return !!(self::FLAG($info['indic_category']) & $flags);
  1067. }
  1068. public static function is_joiner($info)
  1069. {
  1070. return self::is_one_of($info, (self::FLAG(self::OT_ZWJ) | self::FLAG(self::OT_ZWNJ)));
  1071. }
  1072. /* Vowels and placeholders treated as if they were consonants. */
  1073. public static function is_consonant($info)
  1074. {
  1075. return self::is_one_of($info, (self::FLAG(self::OT_C) | self::FLAG(self::OT_CM) | self::FLAG(self::OT_Ra) | self::FLAG(self::OT_V) | self::FLAG(self::OT_NBSP) | self::FLAG(self::OT_DOTTEDCIRCLE)));
  1076. }
  1077. public static function is_halant_or_coeng($info)
  1078. {
  1079. return self::is_one_of($info, (self::FLAG(self::OT_H) | self::FLAG(self::OT_Coeng)));
  1080. }
  1081. // From hb-private.hh
  1082. public static function in_range($u, $lo, $hi)
  1083. {
  1084. if ((($lo ^ $hi) & $lo) == 0 && (($lo ^ $hi) & $hi) == ($lo ^ $hi) && (($lo ^ $hi) & (($lo ^ $hi) + 1)) == 0)
  1085. return ($u & ~($lo ^ $hi)) == $lo;
  1086. else
  1087. return $lo <= $u && $u <= $hi;
  1088. }
  1089. // From hb-private.hh
  1090. public static function FLAG($x)
  1091. {
  1092. return (1 << ($x));
  1093. }
  1094. // BELOW from hb-ot-shape-complex-indic.cc
  1095. /*
  1096. * Indic configurations.
  1097. */
  1098. // base_position
  1099. const BASE_POS_FIRST = 0;
  1100. const BASE_POS_LAST = 1;
  1101. // reph_position
  1102. const REPH_POS_DEFAULT = 10; // POS_BEFORE_POST,
  1103. const REPH_POS_AFTER_MAIN = 5; // POS_AFTER_MAIN,
  1104. const REPH_POS_BEFORE_SUB = 7; // POS_BEFORE_SUB,
  1105. const REPH_POS_AFTER_SUB = 9; // POS_AFTER_SUB,
  1106. const REPH_POS_BEFORE_POST = 10; // POS_BEFORE_POST,
  1107. const REPH_POS_AFTER_POST = 12; // POS_AFTER_POST
  1108. // reph_mode
  1109. const REPH_MODE_IMPLICIT = 0; /* Reph formed out of initial Ra,H sequence. */
  1110. const REPH_MODE_EXPLICIT = 1; /* Reph formed out of initial Ra,H,ZWJ sequence. */
  1111. const REPH_MODE_VIS_REPHA = 2; /* Encoded Repha character, no reordering needed. */
  1112. const REPH_MODE_LOG_REPHA = 3; /* Encoded Repha character, needs reordering. */
  1113. /*
  1114. struct of indic_configs{
  1115. KEY - script;
  1116. 0 - has_old_spec;
  1117. 1 - virama;
  1118. 2 - base_pos;
  1119. 3 - reph_pos;
  1120. 4 - reph_mode;
  1121. };
  1122. */
  1123. public static $indic_configs = array(/* index is SCRIPT_number from UCDN */
  1124. 9 => array(true, 0x094D, 1, 10, 0),
  1125. 10 => array(true, 0x09CD, 1, 9, 0),
  1126. 11 => array(true, 0x0A4D, 1, 7, 0),
  1127. 12 => array(true, 0x0ACD, 1, 10, 0),
  1128. 13 => array(true, 0x0B4D, 1, 5, 0),
  1129. 14 => array(true, 0x0BCD, 1, 12, 0),
  1130. 15 => array(true, 0x0C4D, 1, 12, 1),
  1131. 16 => array(true, 0x0CCD, 1, 12, 0),
  1132. 17 => array(true, 0x0D4D, 1, 5, 3),
  1133. 18 => array(false, 0x0DCA, 0, 5, 1), /* Sinhala */
  1134. 30 => array(false, 0x17D2, 0, 10, 2), /* Khmer */
  1135. 84 => array(false, 0xA9C0, 1, 10, 0), /* Javanese */
  1136. );
  1137. /*
  1138. // from "hb-ot-shape-complex-indic-table.cc"
  1139. const ISC_A = 0; // INDIC_SYLLABIC_CATEGORY_AVAGRAHA Avagraha
  1140. const ISC_Bi = 8; // INDIC_SYLLABIC_CATEGORY_BINDU Bindu
  1141. const ISC_C = 1; // INDIC_SYLLABIC_CATEGORY_CONSONANT Consonant
  1142. const ISC_CD = 1; // INDIC_SYLLABIC_CATEGORY_CONSONANT_DEAD Consonant_Dead
  1143. const ISC_CF = 17; // INDIC_SYLLABIC_CATEGORY_CONSONANT_FINAL Consonant_Final
  1144. const ISC_CHL = 1; // INDIC_SYLLABIC_CATEGORY_CONSONANT_HEAD_LETTER Consonant_Head_Letter
  1145. const ISC_CM = 17; // INDIC_SYLLABIC_CATEGORY_CONSONANT_MEDIAL Consonant_Medial
  1146. const ISC_CP = 11; // INDIC_SYLLABIC_CATEGORY_CONSONANT_PLACEHOLDER Consonant_Placeholder
  1147. const ISC_CR = 15; // INDIC_SYLLABIC_CATEGORY_CONSONANT_REPHA Consonant_Repha
  1148. const ISC_CS = 1; // INDIC_SYLLABIC_CATEGORY_CONSONANT_SUBJOINED Consonant_Subjoined
  1149. const ISC_ML = 0; // INDIC_SYLLABIC_CATEGORY_MODIFYING_LETTER Modifying_Letter
  1150. const ISC_N = 3; // INDIC_SYLLABIC_CATEGORY_NUKTA Nukta
  1151. const ISC_x = 0; // INDIC_SYLLABIC_CATEGORY_OTHER Other
  1152. const ISC_RS = 13; // INDIC_SYLLABIC_CATEGORY_REGISTER_SHIFTER Register_Shifter
  1153. const ISC_TL = 0; // INDIC_SYLLABIC_CATEGORY_TONE_LETTER Tone_Letter
  1154. const ISC_TM = 3; // INDIC_SYLLABIC_CATEGORY_TONE_MARK Tone_Mark
  1155. const ISC_V = 4; // INDIC_SYLLABIC_CATEGORY_VIRAMA Virama
  1156. const ISC_Vs = 8; // INDIC_SYLLABIC_CATEGORY_VISARGA Visarga
  1157. const ISC_Vo = 2; // INDIC_SYLLABIC_CATEGORY_VOWEL Vowel
  1158. const ISC_M = 7; // INDIC_SYLLABIC_CATEGORY_VOWEL_DEPENDENT Vowel_Dependent
  1159. const ISC_VI = 2; // INDIC_SYLLABIC_CATEGORY_VOWEL_INDEPENDENT Vowel_Independent
  1160. const IMC_B = 8; // INDIC_MATRA_CATEGORY_BOTTOM Bottom
  1161. const IMC_BR = 11; // INDIC_MATRA_CATEGORY_BOTTOM_AND_RIGHT Bottom_And_Right
  1162. const IMC_I = 15; // INDIC_MATRA_CATEGORY_INVISIBLE Invisible
  1163. const IMC_L = 3; // INDIC_MATRA_CATEGORY_LEFT Left
  1164. const IMC_LR = 11; // INDIC_MATRA_CATEGORY_LEFT_AND_RIGHT Left_And_Right
  1165. const IMC_x = 15; // INDIC_MATRA_CATEGORY_NOT_APPLICABLE Not_Applicable
  1166. const IMC_O = 5; // INDIC_MATRA_CATEGORY_OVERSTRUCK Overstruck
  1167. const IMC_R = 11; // INDIC_MATRA_CATEGORY_RIGHT Right
  1168. const IMC_T = 6; // INDIC_MATRA_CATEGORY_TOP Top
  1169. const IMC_TB = 8; // INDIC_MATRA_CATEGORY_TOP_AND_BOTTOM Top_And_Bottom
  1170. const IMC_TBR = 11; // INDIC_MATRA_CATEGORY_TOP_AND_BOTTOM_AND_RIGHT Top_And_Bottom_And_Right
  1171. const IMC_TL = 6; // INDIC_MATRA_CATEGORY_TOP_AND_LEFT Top_And_Left
  1172. const IMC_TLR = 11; // INDIC_MATRA_CATEGORY_TOP_AND_LEFT_AND_RIGHT Top_And_Left_And_Right
  1173. const IMC_TR = 11; // INDIC_MATRA_CATEGORY_TOP_AND_RIGHT Top_And_Right
  1174. const IMC_VOL = 2; // INDIC_MATRA_CATEGORY_VISUAL_ORDER_LEFT Visual_Order_Left
  1175. If in original table = _(C,x), that = ISC_C,IMC_x
  1176. Value is IMC_x << 8 (or IMC_x * 256) = 3840
  1177. plus ISC_C = 1, so = 3841
  1178. */
  1179. public static $indic_table = array(
  1180. /* Devanagari (0900..097F) */
  1181. /* 0900 */ 3848, 3848, 3848, 3848, 3842, 3842, 3842, 3842,
  1182. /* 0908 */ 3842, 3842, 3842, 3842, 3842, 3842, 3842, 3842,
  1183. /* 0910 */ 3842, 3842, 3842, 3842, 3842, 3841, 3841, 3841,
  1184. /* 0918 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1185. /* 0920 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1186. /* 0928 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1187. /* 0930 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1188. /* 0938 */ 3841, 3841, 1543, 2823, 3843, 3840, 2823, 775,
  1189. /* 0940 */ 2823, 2055, 2055, 2055, 2055, 1543, 1543, 1543,
  1190. /* 0948 */ 1543, 2823, 2823, 2823, 2823, 2052, 775, 2823,
  1191. /* 0950 */ 3840, 3840, 3840, 3840, 3840, 1543, 2055, 2055,
  1192. /* 0958 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1193. /* 0960 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1194. /* 0968 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1195. /* 0970 */ 3840, 3840, 3842, 3842, 3842, 3842, 3842, 3842,
  1196. /* 0978 */ 3840, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1197. /* Bengali (0980..09FF) */
  1198. /* 0980 */ 3840, 3848, 3848, 3848, 3840, 3842, 3842, 3842,
  1199. /* 0988 */ 3842, 3842, 3842, 3842, 3842, 3840, 3840, 3842,
  1200. /* 0990 */ 3842, 3840, 3840, 3842, 3842, 3841, 3841, 3841,
  1201. /* 0998 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1202. /* 09A0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1203. /* 09A8 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1204. /* 09B0 */ 3841, 3840, 3841, 3840, 3840, 3840, 3841, 3841,
  1205. /* 09B8 */ 3841, 3841, 3840, 3840, 3843, 3840, 2823, 775,
  1206. /* 09C0 */ 2823, 2055, 2055, 2055, 2055, 3840, 3840, 775,
  1207. /* 09C8 */ 775, 3840, 3840, 2823, 2823, 2052, 3841, 3840,
  1208. /* 09D0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 2823,
  1209. /* 09D8 */ 3840, 3840, 3840, 3840, 3841, 3841, 3840, 3841,
  1210. /* 09E0 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1211. /* 09E8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1212. /* 09F0 */ 3841, 3841, 3840, 3840, 3840, 3840, 3840, 3840,
  1213. /* 09F8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1214. /* Gurmukhi (0A00..0A7F) */
  1215. /* 0A00 */ 3840, 3848, 3848, 3848, 3840, 3842, 3842, 3842,
  1216. /* 0A08 */ 3842, 3842, 3842, 3840, 3840, 3840, 3840, 3842,
  1217. /* 0A10 */ 3842, 3840, 3840, 3842, 3842, 3841, 3841, 3841,
  1218. /* 0A18 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1219. /* 0A20 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1220. /* 0A28 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1221. /* 0A30 */ 3841, 3840, 3841, 3841, 3840, 3841, 3841, 3840,
  1222. /* 0A38 */ 3841, 3841, 3840, 3840, 3843, 3840, 2823, 775,
  1223. /* 0A40 */ 2823, 2055, 2055, 3840, 3840, 3840, 3840, 1543,
  1224. /* 0A48 */ 1543, 3840, 3840, 1543, 1543, 2052, 3840, 3840,
  1225. /* 0A50 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1226. /* 0A58 */ 3840, 3841, 3841, 3841, 3841, 3840, 3841, 3840,
  1227. /* 0A60 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1228. /* 0A68 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1229. /* 0A70 */ 3848, 3840, 13841, 13841, 3840, 3857, 3840, 3840,
  1230. /* 0A78 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1231. /* Gujarati (0A80..0AFF) */
  1232. /* 0A80 */ 3840, 3848, 3848, 3848, 3840, 3842, 3842, 3842,
  1233. /* 0A88 */ 3842, 3842, 3842, 3842, 3842, 3842, 3840, 3842,
  1234. /* 0A90 */ 3842, 3842, 3840, 3842, 3842, 3841, 3841, 3841,
  1235. /* 0A98 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1236. /* 0AA0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1237. /* 0AA8 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1238. /* 0AB0 */ 3841, 3840, 3841, 3841, 3840, 3841, 3841, 3841,
  1239. /* 0AB8 */ 3841, 3841, 3840, 3840, 3843, 3840, 2823, 775,
  1240. /* 0AC0 */ 2823, 2055, 2055, 2055, 2055, 1543, 3840, 1543,
  1241. /* 0AC8 */ 1543, 2823, 3840, 2823, 2823, 2052, 3840, 3840,
  1242. /* 0AD0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1243. /* 0AD8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1244. /* 0AE0 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1245. /* 0AE8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1246. /* 0AF0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1247. /* 0AF8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1248. /* Oriya (0B00..0B7F) */
  1249. /* 0B00 */ 3840, 3848, 3848, 3848, 3840, 3842, 3842, 3842,
  1250. /* 0B08 */ 3842, 3842, 3842, 3842, 3842, 3840, 3840, 3842,
  1251. /* 0B10 */ 3842, 3840, 3840, 3842, 3842, 3841, 3841, 3841,
  1252. /* 0B18 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1253. /* 0B20 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1254. /* 0B28 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1255. /* 0B30 */ 3841, 3840, 3841, 3841, 3840, 3841, 3841, 3841,
  1256. /* 0B38 */ 3841, 3841, 3840, 3840, 3843, 3840, 2823, 1543,
  1257. /* 0B40 */ 2823, 2055, 2055, 2055, 2055, 3840, 3840, 775,
  1258. /* 0B48 */ 1543, 3840, 3840, 2823, 2823, 2052, 3840, 3840,
  1259. /* 0B50 */ 3840, 3840, 3840, 3840, 3840, 3840, 1543, 2823,
  1260. /* 0B58 */ 3840, 3840, 3840, 3840, 3841, 3841, 3840, 3841,
  1261. /* 0B60 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1262. /* 0B68 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1263. /* 0B70 */ 3840, 3841, 3840, 3840, 3840, 3840, 3840, 3840,
  1264. /* 0B78 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1265. /* Tamil (0B80..0BFF) */
  1266. /* 0B80 */ 3840, 3840, 3848, 3840, 3840, 3842, 3842, 3842,
  1267. /* 0B88 */ 3842, 3842, 3842, 3840, 3840, 3840, 3842, 3842,
  1268. /* 0B90 */ 3842, 3840, 3842, 3842, 3842, 3841, 3840, 3840,
  1269. /* 0B98 */ 3840, 3841, 3841, 3840, 3841, 3840, 3841, 3841,
  1270. /* 0BA0 */ 3840, 3840, 3840, 3841, 3841, 3840, 3840, 3840,
  1271. /* 0BA8 */ 3841, 3841, 3841, 3840, 3840, 3840, 3841, 3841,
  1272. /* 0BB0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1273. /* 0BB8 */ 3841, 3841, 3840, 3840, 3840, 3840, 2823, 2823,
  1274. /* 0BC0 */ 1543, 2055, 2055, 3840, 3840, 3840, 775, 775,
  1275. /* 0BC8 */ 775, 3840, 2823, 2823, 2823, 1540, 3840, 3840,
  1276. /* 0BD0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 2823,
  1277. /* 0BD8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1278. /* 0BE0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1279. /* 0BE8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1280. /* 0BF0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1281. /* 0BF8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1282. /* Telugu (0C00..0C7F) */
  1283. /* 0C00 */ 3840, 3848, 3848, 3848, 3840, 3842, 3842, 3842,
  1284. /* 0C08 */ 3842, 3842, 3842, 3842, 3842, 3840, 3842, 3842,
  1285. /* 0C10 */ 3842, 3840, 3842, 3842, 3842, 3841, 3841, 3841,
  1286. /* 0C18 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1287. /* 0C20 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1288. /* 0C28 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1289. /* 0C30 */ 3841, 3841, 3841, 3841, 3840, 3841, 3841, 3841,
  1290. /* 0C38 */ 3841, 3841, 3840, 3840, 3840, 3840, 1543, 1543,
  1291. /* 0C40 */ 1543, 2823, 2823, 2823, 2823, 3840, 1543, 1543,
  1292. /* 0C48 */ 2055, 3840, 1543, 1543, 1543, 1540, 3840, 3840,
  1293. /* 0C50 */ 3840, 3840, 3840, 3840, 3840, 1543, 2055, 3840,
  1294. /* 0C58 */ 3841, 3841, 3840, 3840, 3840, 3840, 3840, 3840,
  1295. /* 0C60 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1296. /* 0C68 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1297. /* 0C70 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1298. /* 0C78 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1299. /* Kannada (0C80..0CFF) */
  1300. /* 0C80 */ 3840, 3840, 3848, 3848, 3840, 3842, 3842, 3842,
  1301. /* 0C88 */ 3842, 3842, 3842, 3842, 3842, 3840, 3842, 3842,
  1302. /* 0C90 */ 3842, 3840, 3842, 3842, 3842, 3841, 3841, 3841,
  1303. /* 0C98 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1304. /* 0CA0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1305. /* 0CA8 */ 3841, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1306. /* 0CB0 */ 3841, 3841, 3841, 3841, 3840, 3841, 3841, 3841,
  1307. /* 0CB8 */ 3841, 3841, 3840, 3840, 3843, 3840, 2823, 1543,
  1308. /* 0CC0 */ 2823, 2823, 2823, 2823, 2823, 3840, 1543, 2823,
  1309. /* 0CC8 */ 2823, 3840, 2823, 2823, 1543, 1540, 3840, 3840,
  1310. /* 0CD0 */ 3840, 3840, 3840, 3840, 3840, 2823, 2823, 3840,
  1311. /* 0CD8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3841, 3840,
  1312. /* 0CE0 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1313. /* 0CE8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1314. /* 0CF0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1315. /* 0CF8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1316. /* Malayalam (0D00..0D7F) */
  1317. /* 0D00 */ 3840, 3840, 3848, 3848, 3840, 3842, 3842, 3842,
  1318. /* 0D08 */ 3842, 3842, 3842, 3842, 3842, 3840, 3842, 3842,
  1319. /* 0D10 */ 3842, 3840, 3842, 3842, 3842, 3841, 3841, 3841,
  1320. /* 0D18 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1321. /* 0D20 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1322. /* 0D28 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1323. /* 0D30 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1324. /* 0D38 */ 3841, 3841, 3841, 3840, 3840, 3840, 2823, 2823,
  1325. /* 0D40 */ 2823, 2823, 2823, 2055, 2055, 3840, 775, 775,
  1326. /* 0D48 */ 775, 3840, 2823, 2823, 2823, 1540, 3855, 3840,
  1327. /* 0D50 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 2823,
  1328. /* 0D58 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1329. /* 0D60 */ 3842, 3842, 2055, 2055, 3840, 3840, 3840, 3840,
  1330. /* 0D68 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1331. /* 0D70 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1332. /* 0D78 */ 3840, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1333. /* Sinhala (0D80..0DFF) */
  1334. /* 0D80 */ 3840, 3840, 3848, 3848, 3840, 3842, 3842, 3842,
  1335. /* 0D88 */ 3842, 3842, 3842, 3842, 3842, 3842, 3842, 3842,
  1336. /* 0D90 */ 3842, 3842, 3842, 3842, 3842, 3842, 3842, 3840,
  1337. /* 0D98 */ 3840, 3840, 3841, 3841, 3841, 3841, 3841, 3841,
  1338. /* 0DA0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1339. /* 0DA8 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1340. /* 0DB0 */ 3841, 3841, 3840, 3841, 3841, 3841, 3841, 3841,
  1341. /* 0DB8 */ 3841, 3841, 3841, 3841, 3840, 3841, 3840, 3840,
  1342. /* 0DC0 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3840,
  1343. /* 0DC8 */ 3840, 3840, 1540, 3840, 3840, 3840, 3840, 2823,
  1344. /* 0DD0 */ 2823, 2823, 1543, 1543, 2055, 3840, 2055, 3840,
  1345. /* 0DD8 */ 2823, 775, 1543, 775, 2823, 2823, 2823, 2823,
  1346. /* 0DE0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1347. /* 0DE8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1348. /* 0DF0 */ 3840, 3840, 2823, 2823, 3840, 3840, 3840, 3840,
  1349. /* 0DF8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1350. /* Vedic Extensions (1CD0..1CFF) */
  1351. /* 1CD0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1352. /* 1CD8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1353. /* 1CE0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1354. /* 1CE8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1355. /* 1CF0 */ 3840, 3840, 3848, 3848, 3840, 3840, 3840, 3840,
  1356. /* 1CF8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1357. );
  1358. public static $khmer_table = array(
  1359. /* Khmer (1780..17FF) */
  1360. /* 1780 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1361. /* 1788 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1362. /* 1790 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1363. /* 1798 */ 3841, 3841, 3841, 3841, 3841, 3841, 3841, 3841,
  1364. /* 17A0 */ 3841, 3841, 3841, 3842, 3842, 3842, 3842, 3842,
  1365. /* 17A8 */ 3842, 3842, 3842, 3842, 3842, 3842, 3842, 3842,
  1366. /* 17B0 */ 3842, 3842, 3842, 3842, 3840, 3840, 2823, 1543,
  1367. /* 17B8 */ 1543, 1543, 1543, 2055, 2055, 2055, 1543, 2823,
  1368. /* 17C0 */ 2823, 775, 775, 775, 2823, 2823, 3848, 3848,
  1369. /* 17C8 */ 2823, 3853, 3853, 3840, 3855, 3840, 3840, 3840,
  1370. /* 17D0 */ 3840, 1540, 3844, 3840, 3840, 3840, 3840, 3840,
  1371. /* 17D8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1372. /* 17E0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1373. /* 17E8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1374. /* 17F0 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1375. /* 17F8 */ 3840, 3840, 3840, 3840, 3840, 3840, 3840, 3840,
  1376. );
  1377. // from "hb-ot-shape-complex-indic-table.cc"
  1378. public static function indic_get_categories($u)
  1379. {
  1380. if (0x0900 <= $u && $u <= 0x0DFF)
  1381. return self::$indic_table[$u - 0x0900 + 0]; // offset 0 for Most "indic"
  1382. if (0x1CD0 <= $u && $u <= 0x1D00)
  1383. return self::$indic_table[$u - 0x1CD0 + 1152]; // offset for Vedic extensions
  1384. if (0x1780 <= $u && $u <= 0x17FF)
  1385. return self::$khmer_table[$u - 0x1780]; // Khmer
  1386. if ($u == 0x00A0)
  1387. return 3851; // (ISC_CP | (IMC_x << 8))
  1388. if ($u == 0x25CC)
  1389. return 3851; // (ISC_CP | (IMC_x << 8))
  1390. return 3840; // (ISC_x | (IMC_x << 8))
  1391. }
  1392. // BELOW from hb-ot-shape-complex-indic.cc
  1393. /*
  1394. * Indic shaper.
  1395. */
  1396. public static function IN_HALF_BLOCK($u, $Base)
  1397. {
  1398. return (($u & ~0x7F) == $Base);
  1399. }
  1400. public static function IS_DEVA($u)
  1401. {
  1402. return self::IN_HALF_BLOCK($u, 0x0900);
  1403. }
  1404. public static function IS_BENG($u)
  1405. {
  1406. return self::IN_HALF_BLOCK($u, 0x0980);
  1407. }
  1408. public static function IS_GURU($u)
  1409. {
  1410. return self::IN_HALF_BLOCK($u, 0x0A00);
  1411. }
  1412. public static function IS_GUJR($u)
  1413. {
  1414. return self::IN_HALF_BLOCK($u, 0x0A80);
  1415. }
  1416. public static function IS_ORYA($u)
  1417. {
  1418. return self::IN_HALF_BLOCK($u, 0x0B00);
  1419. }
  1420. public static function IS_TAML($u)
  1421. {
  1422. return self::IN_HALF_BLOCK($u, 0x0B80);
  1423. }
  1424. public static function IS_TELU($u)
  1425. {
  1426. return self::IN_HALF_BLOCK($u, 0x0C00);
  1427. }
  1428. public static function IS_KNDA($u)
  1429. {
  1430. return self::IN_HALF_BLOCK($u, 0x0C80);
  1431. }
  1432. public static function IS_MLYM($u)
  1433. {
  1434. return self::IN_HALF_BLOCK($u, 0x0D00);
  1435. }
  1436. public static function IS_SINH($u)
  1437. {
  1438. return self::IN_HALF_BLOCK($u, 0x0D80);
  1439. }
  1440. public static function IS_KHMR($u)
  1441. {
  1442. return self::IN_HALF_BLOCK($u, 0x1780);
  1443. }
  1444. public static function MATRA_POS_LEFT($u)
  1445. {
  1446. return self::POS_PRE_M;
  1447. }
  1448. public static function MATRA_POS_RIGHT($u)
  1449. {
  1450. return
  1451. (self::IS_DEVA($u) ? self::POS_AFTER_SUB :
  1452. (self::IS_BENG($u) ? self::POS_AFTER_POST :
  1453. (self::IS_GURU($u) ? self::POS_AFTER_POST :
  1454. (self::IS_GUJR($u) ? self::POS_AFTER_POST :
  1455. (self::IS_ORYA($u) ? self::POS_AFTER_POST :
  1456. (self::IS_TAML($u) ? self::POS_AFTER_POST :
  1457. (self::IS_TELU($u) ? ($u <= 0x0C42 ? self::POS_BEFORE_SUB : self::POS_AFTER_SUB) :
  1458. (self::IS_KNDA($u) ? ($u < 0x0CC3 || $u > 0xCD6 ? self::POS_BEFORE_SUB : self::POS_AFTER_SUB) :
  1459. (self::IS_MLYM($u) ? self::POS_AFTER_POST :
  1460. (self::IS_SINH($u) ? self::POS_AFTER_SUB :
  1461. (self::IS_KHMR($u) ? self::POS_AFTER_POST :
  1462. self::POS_AFTER_SUB))))))))))); /* default */
  1463. }
  1464. public static function MATRA_POS_TOP($u)
  1465. {
  1466. return /* BENG and MLYM don't have top matras. */
  1467. (self::IS_DEVA($u) ? self::POS_AFTER_SUB :
  1468. (self::IS_GURU($u) ? self::POS_AFTER_POST : /* Deviate from spec */
  1469. (self::IS_GUJR($u) ? self::POS_AFTER_SUB :
  1470. (self::IS_ORYA($u) ? self::POS_AFTER_MAIN :
  1471. (self::IS_TAML($u) ? self::POS_AFTER_SUB :
  1472. (self::IS_TELU($u) ? self::POS_BEFORE_SUB :
  1473. (self::IS_KNDA($u) ? self::POS_BEFORE_SUB :
  1474. (self::IS_SINH($u) ? self::POS_AFTER_SUB :
  1475. (self::IS_KHMR($u) ? self::POS_AFTER_POST :
  1476. self::POS_AFTER_SUB))))))))); /* default */
  1477. }
  1478. public static function MATRA_POS_BOTTOM($u)
  1479. {
  1480. return
  1481. (self::IS_DEVA($u) ? self::POS_AFTER_SUB :
  1482. (self::IS_BENG($u) ? self::POS_AFTER_SUB :
  1483. (self::IS_GURU($u) ? self::POS_AFTER_POST :
  1484. (self::IS_GUJR($u) ? self::POS_AFTER_POST :
  1485. (self::IS_ORYA($u) ? self::POS_AFTER_SUB :
  1486. (self::IS_TAML($u) ? self::POS_AFTER_POST :
  1487. (self::IS_TELU($u) ? self::POS_BEFORE_SUB :
  1488. (self::IS_KNDA($u) ? self::POS_BEFORE_SUB :
  1489. (self::IS_MLYM($u) ? self::POS_AFTER_POST :
  1490. (self::IS_SINH($u) ? self::POS_AFTER_SUB :
  1491. (self::IS_KHMR($u) ? self::POS_AFTER_POST :
  1492. self::POS_AFTER_SUB))))))))))); /* default */
  1493. }
  1494. public static function matra_position($u, $side)
  1495. {
  1496. switch ($side) {
  1497. case self::POS_PRE_C: return self::MATRA_POS_LEFT($u);
  1498. case self::POS_POST_C: return self::MATRA_POS_RIGHT($u);
  1499. case self::POS_ABOVE_C: return self::MATRA_POS_TOP($u);
  1500. case self::POS_BELOW_C: return self::MATRA_POS_BOTTOM($u);
  1501. }
  1502. return $side;
  1503. }
  1504. // vowel matras that have to be split into two parts.
  1505. // From Harfbuzz (old)
  1506. // New HarfBuzz uses /src/hb-ucdn/ucdn.c and unicodedata_db.h for full method of decomposition for all characters
  1507. // Should always fully decompose and then recompose back, but we will just do the split matras
  1508. public static function decompose_indic($ab)
  1509. {
  1510. $sub = array();
  1511. switch ($ab) {
  1512. /*
  1513. * Decompose split matras.
  1514. */
  1515. /* bengali */
  1516. case 0x9cb : $sub[0] = 0x9c7;
  1517. $sub[1] = 0x9be;
  1518. return $sub;
  1519. case 0x9cc : $sub[0] = 0x9c7;
  1520. $sub[1] = 0x9d7;
  1521. return $sub;
  1522. /* oriya */
  1523. case 0xb48 : $sub[0] = 0xb47;
  1524. $sub[1] = 0xb56;
  1525. return $sub;
  1526. case 0xb4b : $sub[0] = 0xb47;
  1527. $sub[1] = 0xb3e;
  1528. return $sub;
  1529. case 0xb4c : $sub[0] = 0xb47;
  1530. $sub[1] = 0xb57;
  1531. return $sub;
  1532. /* tamil */
  1533. case 0xbca : $sub[0] = 0xbc6;
  1534. $sub[1] = 0xbbe;
  1535. return $sub;
  1536. case 0xbcb : $sub[0] = 0xbc7;
  1537. $sub[1] = 0xbbe;
  1538. return $sub;
  1539. case 0xbcc : $sub[0] = 0xbc6;
  1540. $sub[1] = 0xbd7;
  1541. return $sub;
  1542. /* telugu */
  1543. case 0xc48 : $sub[0] = 0xc46;
  1544. $sub[1] = 0xc56;
  1545. return $sub;
  1546. /* kannada */
  1547. case 0xcc0 : $sub[0] = 0xcbf;
  1548. $sub[1] = 0xcd5;
  1549. return $sub;
  1550. case 0xcc7 : $sub[0] = 0xcc6;
  1551. $sub[1] = 0xcd5;
  1552. return $sub;
  1553. case 0xcc8 : $sub[0] = 0xcc6;
  1554. $sub[1] = 0xcd6;
  1555. return $sub;
  1556. case 0xcca : $sub[0] = 0xcc6;
  1557. $sub[1] = 0xcc2;
  1558. return $sub;
  1559. case 0xccb : $sub[0] = 0xcc6;
  1560. $sub[1] = 0xcc2;
  1561. $sub[2] = 0xcd5;
  1562. return $sub;
  1563. /* malayalam */
  1564. case 0xd4a : $sub[0] = 0xd46;
  1565. $sub[1] = 0xd3e;
  1566. return $sub;
  1567. case 0xd4b : $sub[0] = 0xd47;
  1568. $sub[1] = 0xd3e;
  1569. return $sub;
  1570. case 0xd4c : $sub[0] = 0xd46;
  1571. $sub[1] = 0xd57;
  1572. return $sub;
  1573. /* sinhala */
  1574. // NB Some fonts break with these Sinhala decomps (although this is Uniscribe spec)
  1575. // Can check if character would be substituted by pstf and only decompose if true
  1576. // e.g. if (isset($GSUBdata['pstf'][$ab])) - would need to pass $GSUBdata as parameter to this function
  1577. case 0xdda : $sub[0] = 0xdd9;
  1578. $sub[1] = 0xdca;
  1579. return $sub;
  1580. case 0xddc : $sub[0] = 0xdd9;
  1581. $sub[1] = 0xdcf;
  1582. return $sub;
  1583. case 0xddd : $sub[0] = 0xdd9;
  1584. $sub[1] = 0xdcf;
  1585. $sub[2] = 0xdca;
  1586. return $sub;
  1587. case 0xdde : $sub[0] = 0xdd9;
  1588. $sub[1] = 0xddf;
  1589. return $sub;
  1590. /* khmer */
  1591. case 0x17be : $sub[0] = 0x17c1;
  1592. $sub[1] = 0x17be;
  1593. return $sub;
  1594. case 0x17bf : $sub[0] = 0x17c1;
  1595. $sub[1] = 0x17bf;
  1596. return $sub;
  1597. case 0x17c0 : $sub[0] = 0x17c1;
  1598. $sub[1] = 0x17c0;
  1599. return $sub;
  1600. case 0x17c4 : $sub[0] = 0x17c1;
  1601. $sub[1] = 0x17c4;
  1602. return $sub;
  1603. case 0x17c5 : $sub[0] = 0x17c1;
  1604. $sub[1] = 0x17c5;
  1605. return $sub;
  1606. /* tibetan - included here although does not use Inidc shaper in other ways */
  1607. case 0xf73 : $sub[0] = 0xf71;
  1608. $sub[1] = 0xf72;
  1609. return $sub;
  1610. case 0xf75 : $sub[0] = 0xf71;
  1611. $sub[1] = 0xf74;
  1612. return $sub;
  1613. case 0xf76 : $sub[0] = 0xfb2;
  1614. $sub[1] = 0xf80;
  1615. return $sub;
  1616. case 0xf77 : $sub[0] = 0xfb2;
  1617. $sub[1] = 0xf81;
  1618. return $sub;
  1619. case 0xf78 : $sub[0] = 0xfb3;
  1620. $sub[1] = 0xf80;
  1621. return $sub;
  1622. case 0xf79 : $sub[0] = 0xfb3;
  1623. $sub[1] = 0xf71;
  1624. $sub[2] = 0xf80;
  1625. return $sub;
  1626. case 0xf81 : $sub[0] = 0xf71;
  1627. $sub[1] = 0xf80;
  1628. return $sub;
  1629. }
  1630. return false;
  1631. }
  1632. public static function bubble_sort(&$arr, $start, $len)
  1633. {
  1634. if ($len < 2) {
  1635. return;
  1636. }
  1637. $k = $start + $len - 2;
  1638. while ($k >= $start) {
  1639. for ($j = $start; $j <= $k; $j++) {
  1640. if ($arr[$j]['indic_position'] > $arr[$j + 1]['indic_position']) {
  1641. $t = $arr[$j];
  1642. $arr[$j] = $arr[$j + 1];
  1643. $arr[$j + 1] = $t;
  1644. }
  1645. }
  1646. $k--;
  1647. }
  1648. }
  1649. }