I18N_Arabic

# Source for file Glyphs.php

Documentation is available at Glyphs.php

1. <?php
2. /**
3. * ----------------------------------------------------------------------
4. *
5. * Copyright (c) 2006-2016 Khaled Al-Sham'aa.
6. *
7. * http://www.ar-php.org
8. *
9. * PHP Version 5
10. *
11. * ----------------------------------------------------------------------
12. *
13. * LICENSE
14. *
15. * This program is open source product; you can redistribute it and/or
16. * modify it under the terms of the GNU Lesser General Public License (LGPL)
17. * as published by the Free Software Foundation; either version 3
18. * of the License, or (at your option) any later version.
19. *
20. * This program is distributed in the hope that it will be useful,
21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23. * GNU Lesser General Public License for more details.
24. *
25. * You should have received a copy of the GNU Lesser General Public License
26. * along with this program. If not, see <http://www.gnu.org/licenses/lgpl.txt>.
27. *
28. * ----------------------------------------------------------------------
29. *
30. * Class Name: Arabic Glyphs is a simple class to render Arabic text
31. *
32. * Filename: Glyphs.php
33. *
34. * Original Author(s): Khaled Al-Sham'aa <khaled@ar-php.org>
35. *
36. * Purpose: This class takes Arabic text (encoded in Windows-1256 character
37. * set) as input and performs Arabic glyph joining on it and outputs
38. * a UTF-8 hexadecimals stream that is no longer logically arranged
39. * but in a visual order which gives readable results when formatted
40. * with a simple Unicode rendering just like GD and UFPDF libraries
41. * that does not handle basic connecting glyphs of Arabic language
42. * yet but simply outputs all stand alone glyphs in left-to-right
43. * order.
44. *
45. * ----------------------------------------------------------------------
46. *
47. * Arabic Glyphs is class to render Arabic text
48. *
49. * PHP class to render Arabic text by performs Arabic glyph joining on it,
50. * then output a UTF-8 hexadecimals stream gives readable results on PHP
51. * libraries supports UTF-8.
52. *
53. * Example:
54. * <code>
55. * include('./I18N/Arabic.php');
56. * $obj = new I18N_Arabic('Glyphs');
57. *
58. * $text = $obj->utf8Glyphs($text);
59. *
60. * imagettftext($im, 20, 0, 200, 100, $black, $font, $text);
61. * </code>
62. *
63. * @category I18N
64. * @package I18N_Arabic
65. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
66. * @copyright 2006-2016 Khaled Al-Sham'aa
67. *
68. * @license LGPL <http://www.gnu.org/licenses/lgpl.txt>
69. * @link http://www.ar-php.org
70. */
72. /**
73. * This PHP class render Arabic text by performs Arabic glyph joining on it
74. *
75. * @category I18N
76. * @package I18N_Arabic
77. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
78. * @copyright 2006-2016 Khaled Al-Sham'aa
79. *
80. * @license LGPL <http://www.gnu.org/licenses/lgpl.txt>
81. * @link http://www.ar-php.org
82. */
83. class I18N_Arabic_Glyphs
84. {
85. private $_glyphs = null;
86. private $_hex = null;
87. private $_prevLink = null;
88. private $_nextLink = null;
89. private $_vowel = null;
91. /**
92. * Loads initialize values
93. *
94. * @ignore
95. */
96. public function __construct()
97. {
98. $this->_prevLink = '،؟؛ـئبتثجحخسشصضطظعغفقكلمنهي';
99. $this->_nextLink = 'ـآأؤإائبةتثجحخدذرز';
100. $this->_nextLink .= 'سشصضطظعغفقكلمنهوىي';
101. $this->_vowel = 'ًٌٍَُِّْ';
103. /*
104. $this->_glyphs['ً'] = array('FE70','FE71');
105. $this->_glyphs['ٌ'] = array('FE72','FE72');
106. $this->_glyphs['ٍ'] = array('FE74','FE74');
107. $this->_glyphs['َ'] = array('FE76','FE77');
108. $this->_glyphs['ُ'] = array('FE78','FE79');
109. $this->_glyphs['ِ'] = array('FE7A','FE7B');
110. $this->_glyphs['ّ'] = array('FE7C','FE7D');
111. $this->_glyphs['ْ'] = array('FE7E','FE7E');
112. */
114. $this->_glyphs = 'ًٌٍَُِّْٰ';
115. $this->_hex = '064B064B064B064B064C064C064C064C064D064D064D064D064E064E';
116. $this->_hex .= '064E064E064F064F064F064F06500650065006500651065106510651';
117. $this->_hex .= '06520652065206520670067006700670';
119. $this->_glyphs .= 'ءآأؤإئاب';
120. $this->_hex .= 'FE80FE80FE80FE80FE81FE82FE81FE82FE83FE84FE83FE84FE85FE86';
121. $this->_hex .= 'FE85FE86FE87FE88FE87FE88FE89FE8AFE8BFE8CFE8DFE8EFE8DFE8E';
122. $this->_hex .= 'FE8FFE90FE91FE92';
124. $this->_glyphs .= 'ةتثجحخدذ';
125. $this->_hex .= 'FE93FE94FE93FE94FE95FE96FE97FE98FE99FE9AFE9BFE9CFE9DFE9E';
126. $this->_hex .= 'FE9FFEA0FEA1FEA2FEA3FEA4FEA5FEA6FEA7FEA8FEA9FEAAFEA9FEAA';
127. $this->_hex .= 'FEABFEACFEABFEAC';
129. $this->_glyphs .= 'رزسشصضطظ';
130. $this->_hex .= 'FEADFEAEFEADFEAEFEAFFEB0FEAFFEB0FEB1FEB2FEB3FEB4FEB5FEB6';
131. $this->_hex .= 'FEB7FEB8FEB9FEBAFEBBFEBCFEBDFEBEFEBFFEC0FEC1FEC2FEC3FEC4';
132. $this->_hex .= 'FEC5FEC6FEC7FEC8';
134. $this->_glyphs .= 'عغفقكلمن';
135. $this->_hex .= 'FEC9FECAFECBFECCFECDFECEFECFFED0FED1FED2FED3FED4FED5FED6';
136. $this->_hex .= 'FED7FED8FED9FEDAFEDBFEDCFEDDFEDEFEDFFEE0FEE1FEE2FEE3FEE4';
137. $this->_hex .= 'FEE5FEE6FEE7FEE8';
139. $this->_glyphs .= 'هوىيـ،؟؛';
140. $this->_hex .= 'FEE9FEEAFEEBFEECFEEDFEEEFEEDFEEEFEEFFEF0FEEFFEF0FEF1FEF2';
141. $this->_hex .= 'FEF3FEF40640064006400640060C060C060C060C061F061F061F061F';
142. $this->_hex .= '061B061B061B061B';
144. // Support the extra 4 Persian letters (p), (ch), (zh) and (g)
145. // This needs value in getGlyphs function to be 52 instead of 48
146. // $this->_glyphs .= chr(129).chr(141).chr(142).chr(144);
147. // $this->_hex .= 'FB56FB57FB58FB59FB7AFB7BFB7CFB7DFB8AFB8BFB8AFB8BFB92';
148. // $this->_hex .= 'FB93FB94FB95';
149. //
150. // $this->_prevLink .= chr(129).chr(141).chr(142).chr(144);
151. // $this->_nextLink .= chr(129).chr(141).chr(142).chr(144);
152. //
153. // Example: $text = 'نمونة قلم: لاگچ ژافپ';
154. // Email Yossi Beck <yosbeck@gmail.com> ask him to save that example
155. // string using ANSI encoding in Notepad
156. $this->_glyphs .= '';
157. $this->_hex .= '';
159. $this->_glyphs .= 'لآلألإلا';
160. $this->_hex .= 'FEF5FEF6FEF5FEF6FEF7FEF8FEF7FEF8FEF9FEFAFEF9FEFAFEFBFEFC';
161. $this->_hex .= 'FEFBFEFC';
162. }
164. /**
165. * Get glyphs
166. *
167. * @param string $char Char
168. * @param integer $type Type
169. *
170. * @return string
171. */
172. protected function getGlyphs($char, $type)
173. {
175. $pos = mb_strpos($this->_glyphs, $char);
177. if ($pos > 49) {
178. $pos = ($pos-49)/2 + 49;
179. }
181. $pos = $pos*16 + $type*4;
183. return substr($this->_hex, $pos, 4);
184. }
186. /**
187. * Convert Arabic Windows-1256 charset string into glyph joining in UTF-8
188. * hexadecimals stream
189. *
190. * @param string $str Arabic string in Windows-1256 charset
191. *
192. * @return string Arabic glyph joining in UTF-8 hexadecimals stream
193. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
194. */
195. protected function preConvert($str)
196. {
197. $crntChar = null;
198. $prevChar = null;
199. $nextChar = null;
200. $output = '';
202. $_temp = mb_strlen($str);
204. for ($i = 0; $i < $_temp; $i++) {
205. $chars[] = mb_substr($str, $i, 1);
206. }
208. $max = count($chars);
210. for ($i = $max - 1; $i >= 0; $i--) {
211. $crntChar = $chars[$i];
212. $prevChar = ' ';
214. if ($i > 0) {
215. $prevChar = $chars[$i - 1];
216. }
218. if ($prevChar && mb_strpos($this->_vowel, $prevChar) !== false) {
219. $prevChar = $chars[$i - 2];
220. if ($prevChar && mb_strpos($this->_vowel, $prevChar) !== false) {
221. $prevChar = $chars[$i - 3];
222. }
223. }
225. $Reversed = false;
226. $flip_arr = ')]>}';
227. $ReversedChr = '([<{';
229. if ($crntChar && mb_strpos($flip_arr, $crntChar) !== false) {
230. $crntChar = $ReversedChr[mb_strpos($flip_arr, $crntChar)];
231. $Reversed = true;
232. } else {
233. $Reversed = false;
234. }
236. if ($crntChar && !$Reversed
237. && (mb_strpos($ReversedChr, $crntChar) !== false)
238. ) {
239. $crntChar = $flip_arr[mb_strpos($ReversedChr, $crntChar)];
240. }
242. if (ord($crntChar) < 128) {
243. $output .= $crntChar;
244. $nextChar = $crntChar;
245. continue;
246. }
248. if ($crntChar == 'ل' && isset($chars[$i + 1])
249. && (mb_strpos('آأإا', $chars[$i + 1]) !== false)
250. ) {
251. continue;
252. }
254. if ($crntChar && mb_strpos($this->_vowel, $crntChar) !== false) {
255. if (isset($chars[$i + 1])
256. && (mb_strpos($this->_nextLink, $chars[$i + 1]) !== false)
257. && (mb_strpos($this->_prevLink, $prevChar) !== false)
258. ) {
259. $output .= '&#x' . $this->getGlyphs($crntChar, 1) . ';';
260. } else {
261. $output .= '&#x' . $this->getGlyphs($crntChar, 0) . ';';
262. }
263. continue;
264. }
266. $form = 0;
268. if (($prevChar == 'لا' || $prevChar == 'لآ' || $prevChar == 'لأ'
269. || $prevChar == 'لإ' || $prevChar == 'ل')
270. && (mb_strpos('آأإا', $crntChar) !== false)
271. ) {
272. if (mb_strpos($this->_prevLink, $chars[$i - 2]) !== false) {
273. $form++;
274. }
276. if (mb_strpos($this->_vowel, $chars[$i - 1])) {
277. $output .= '&#x';
278. $output .= $this->getGlyphs($crntChar, $form).';';
279. } else {
280. $output .= '&#x';
281. $output .= $this->getGlyphs($prevChar.$crntChar, $form).';';
282. }
283. $nextChar = $prevChar;
284. continue;
285. }
287. if ($prevChar && mb_strpos($this->_prevLink, $prevChar) !== false) {
288. $form++;
289. }
291. if ($nextChar && mb_strpos($this->_nextLink, $nextChar) !== false) {
292. $form += 2;
293. }
295. $output .= '&#x' . $this->getGlyphs($crntChar, $form) . ';';
296. $nextChar = $crntChar;
297. }
299. // from Arabic Presentation Forms-B, Range: FE70-FEFF,
300. // file "UFE70.pdf" (in reversed order)
301. // into Arabic Presentation Forms-A, Range: FB50-FDFF, file "UFB50.pdf"
302. // Example: $output = str_replace('&#xFEA0;&#xFEDF;', '&#xFCC9;', $output);
303. // Lam Jeem
305. $output = $this->decodeEntities($output, $exclude = array('&'));
306. return $output;
307. }
309. /**
310. * Regression analysis calculate roughly the max number of character fit in
311. * one A4 page line for a given font size.
312. *
313. * @param integer $font Font size
314. *
315. * @return integer Maximum number of characters per line
316. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
317. */
318. public function a4MaxChars($font)
319. {
320. $x = 381.6 - 31.57 * $font + 1.182 * pow($font, 2) - 0.02052 *
321. pow($font, 3) + 0.0001342 * pow($font, 4);
322. return floor($x - 2);
323. }
325. /**
326. * Calculate the lines number of given Arabic text and font size that will
327. * fit in A4 page size
328. *
329. * @param string $str Arabic string you would like to split it into lines
330. * @param integer $font Font size
331. *
332. * @return integer Number of lines for a given Arabic string in A4 page size
333. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
334. */
335. public function a4Lines($str, $font)
336. {
337. $str = str_replace(array("\r\n", "\n", "\r"), "\n", $str);
339. $lines = 0;
340. $chars = 0;
341. $words = explode(' ', $str);
342. $w_count = count($words);
343. $max_chars = $this->a4MaxChars($font);
345. for ($i = 0; $i < $w_count; $i++) {
346. $w_len = mb_strlen($words[$i]) + 1;
348. if ($chars + $w_len < $max_chars) {
349. if (mb_strpos($words[$i], "\n") !== false) {
350. $words_nl = explode("\n", $words[$i]);
352. $nl_num = count($words_nl) - 1;
353. for ($j = 1; $j < $nl_num; $j++) {
354. $lines++;
355. }
357. $chars = mb_strlen($words_nl[$nl_num]) + 1;
358. } else {
359. $chars += $w_len;
360. }
361. } else {
362. $lines++;
363. $chars = $w_len;
364. }
365. }
366. $lines++;
368. return $lines;
369. }
371. /**
372. * Convert Arabic Windows-1256 charset string into glyph joining in UTF-8
373. * hexadecimals stream (take care of whole the document including English
374. * sections as well as numbers and arcs etc...)
375. *
376. * @param string $str Arabic string in Windows-1256 charset
377. * @param integer $max_chars Max number of chars you can fit in one line
378. * @param boolean $hindo If true use Hindo digits else use Arabic digits
379. *
380. * @return string Arabic glyph joining in UTF-8 hexadecimals stream (take
381. * care of whole document including English sections as well
382. * as numbers and arcs etc...)
383. * @author Khaled Al-Sham'aa <khaled@ar-php.org>
384. */
385. public function utf8Glyphs($str, $max_chars = 50, $hindo = true)
386. {
387. $str = str_replace(array("\r\n", "\n", "\r"), " \n ", $str);
388. $str = str_replace("\t", " ", $str);
390. $lines = array();
391. $words = explode(' ', $str);
392. $w_count = count($words);
393. $c_chars = 0;
394. $c_words = array();
396. $english = array();
397. $en_index = -1;
399. $en_words = array();
400. $en_stack = array();
402. for ($i = 0; $i < $w_count; $i++) {
403. $pattern = '/^(\n?)';
404. $pattern .= '[a-z\d\\/\@\#\$\%\^\&\*\(\)\_\~\"\'\[\]\{\}\;\,\|\-\.\:!]*';
405. $pattern .= '([\.\:\+\=\-\!،؟]?)$/i';
407. if (preg_match($pattern, $words[$i], $matches)) {
408. if ($matches[1]) {
409. $words[$i] = mb_substr($words[$i], 1).$matches[1];
410. }
411. if ($matches[2]) {
412. $words[$i] = $matches[2].mb_substr($words[$i], 0, -1);
413. }
414. $words[$i] = strrev($words[$i]);
415. array_push($english, $words[$i]);
416. if ($en_index == -1) {
417. $en_index = $i;
418. }
419. $en_words[] = true;
420. } elseif ($en_index != -1) {
421. $en_count = count($english);
423. for ($j = 0; $j < $en_count; $j++) {
424. $words[$en_index + $j] = $english[$en_count - 1 - $j];
425. }
427. $en_index = -1;
428. $english = array();
430. $en_words[] = false;
431. } else {
432. $en_words[] = false;
433. }
434. }
436. if ($en_index != -1) {
437. $en_count = count($english);
439. for ($j = 0; $j < $en_count; $j++) {
440. $words[$en_index + $j] = $english[$en_count - 1 - $j];
441. }
442. }
444. // need more work to fix lines starts by English words
445. if (isset($en_start)) {
446. $last = true;
447. $from = 0;
449. foreach ($en_words as $key => $value) {
450. if ($last !== $value) {
451. $to = $key - 1;
452. array_push($en_stack, array($from, $to));
453. $from = $key;
454. }
455. $last = $value;
456. }
458. array_push($en_stack, array($from, $key));
460. $new_words = array();
462. while (list($from, $to) = array_pop($en_stack)) {
463. for ($i = $from; $i <= $to; $i++) {
464. $new_words[] = $words[$i];
465. }
466. }
468. $words = $new_words;
469. }
471. for ($i = 0; $i < $w_count; $i++) {
472. $w_len = mb_strlen($words[$i]) + 1;
474. if ($c_chars + $w_len < $max_chars) {
475. if (mb_strpos($words[$i], "\n") !== false) {
476. $words_nl = explode("\n", $words[$i]);
478. array_push($c_words, $words_nl[0]);
479. array_push($lines, implode(' ', $c_words));
481. $nl_num = count($words_nl) - 1;
482. for ($j = 1; $j < $nl_num; $j++) {
483. array_push($lines, $words_nl[$j]);
484. }
486. $c_words = array($words_nl[$nl_num]);
487. $c_chars = mb_strlen($words_nl[$nl_num]) + 1;
488. } else {
489. array_push($c_words, $words[$i]);
490. $c_chars += $w_len;
491. }
492. } else {
493. array_push($lines, implode(' ', $c_words));
494. $c_words = array($words[$i]);
495. $c_chars = $w_len;
496. }
497. }
498. array_push($lines, implode(' ', $c_words));
500. $maxLine = count($lines);
501. $output = '';
503. for ($j = $maxLine - 1; $j >= 0; $j--) {
504. $output .= $lines[$j] . "\n";
505. }
507. $output = rtrim($output);
509. $output = $this->preConvert($output);
510. if ($hindo) {
511. $nums = array(
512. '0', '1', '2', '3', '4',
513. '5', '6', '7', '8', '9'
514. );
515. $arNums = array(
516. '٠', '١', '٢', '٣', '٤',
517. '٥', '٦', '٧', '٨', '٩'
518. );
520. foreach ($nums as $k => $v) {
521. $p_nums[$k] = '/'.$v.'/ui';
522. }
523. $output = preg_replace($p_nums, $arNums, $output);
525. foreach ($arNums as $k => $v) {
526. $p_arNums[$k] = '/([a-z-\d]+)'.$v.'/ui';
527. }
528. foreach ($nums as $k => $v) {
529. $r_nums[$k] = '${1}'.$v;
530. }
531. $output = preg_replace($p_arNums, $r_nums, $output);
533. foreach ($arNums as $k => $v) {
534. $p_arNums[$k] = '/'.$v.'([a-z-\d]+)/ui';
535. }
536. foreach ($nums as $k => $v) {
537. $r_nums[$k] = $v.'${1}';
538. }
539. $output = preg_replace($p_arNums, $r_nums, $output);
540. }
542. return $output;
543. }
545. /**
546. * Decode all HTML entities (including numerical ones) to regular UTF-8 bytes.
547. * Double-escaped entities will only be decoded once
548. * ("&amp;lt;" becomes "&lt;", not "<").
549. *
550. * @param string $text The text to decode entities in.
551. * @param array $exclude An array of characters which should not be decoded.
552. * For example, array('<', '&', '"'). This affects
553. * both named and numerical entities.
554. *
555. * @return string
556. */
557. protected function decodeEntities($text, $exclude = array())
558. {
559. static $table;
561. // We store named entities in a table for quick processing.
562. if (!isset($table)) {
563. // Get all named HTML entities.
564. $table = array_flip(get_html_translation_table(HTML_ENTITIES));
566. // PHP gives us ISO-8859-1 data, we need UTF-8.
567. $table = array_map('utf8_encode', $table);
569. // Add apostrophe (XML)
570. $table['&apos;'] = "'";
571. }
572. $newtable = array_diff($table, $exclude);
574. // Use a regexp to select all entities in one pass, to avoid decoding
575. // double-escaped entities twice.
576. //return preg_replace('/&(#x?)?([A-Za-z0-9]+);/e',
577. // '$this->decodeEntities2("$1", "$2", "$0", $newtable,
578. // $exclude)', $text);
580. $pieces = explode('&', $text);
581. $text = array_shift($pieces);
582. foreach ($pieces as $piece) {
583. if ($piece[0] == '#') {
584. if ($piece[1] == 'x') {
585. $one = '#x';
586. } else {
587. $one = '#';
588. }
589. } else {
590. $one = '';
591. }
592. $end = mb_strpos($piece, ';');
593. $start = mb_strlen($one);
595. $two = mb_substr($piece, $start, $end - $start);
596. $zero = '&'.$one.$two.';';
597. $text .= $this->decodeEntities2($one, $two, $zero, $newtable, $exclude).
598. mb_substr($piece, $end+1);
599. }
600. return $text;
601. }
603. /**
604. * Helper function for decodeEntities
605. *
606. * @param string $prefix Prefix
607. * @param string $codepoint Codepoint
608. * @param string $original Original
609. * @param array &$table Store named entities in a table
610. * @param array &$exclude An array of characters which should not be decoded
611. *
612. * @return string
613. */
614. protected function decodeEntities2(
615. $prefix, $codepoint, $original, &$table, &$exclude
616. ) {
617. // Named entity
618. if (!$prefix) {
619. if (isset($table[$original])) {
620. return $table[$original];
621. } else {
622. return $original;
623. }
624. }
626. // Hexadecimal numerical entity
627. if ($prefix == '#x') {
628. $codepoint = base_convert($codepoint, 16, 10);
629. }
631. // Encode codepoint as UTF-8 bytes
632. if ($codepoint < 0x80) {
633. $str = chr($codepoint);
634. } elseif ($codepoint < 0x800) {
635. $str = chr(0xC0 | ($codepoint >> 6)) .
636. chr(0x80 | ($codepoint & 0x3F));
637. } elseif ($codepoint < 0x10000) {
638. $str = chr(0xE0 | ($codepoint >> 12)) .
639. chr(0x80 | (($codepoint >> 6) & 0x3F)) .
640. chr(0x80 | ($codepoint & 0x3F));
641. } elseif ($codepoint < 0x200000) {
642. $str = chr(0xF0 | ($codepoint >> 18)) .
643. chr(0x80 | (($codepoint >> 12) & 0x3F)) .
644. chr(0x80 | (($codepoint >> 6) & 0x3F)) .
645. chr(0x80 | ($codepoint & 0x3F));
646. }
648. // Check for excluded characters
649. if (in_array($str, $exclude)) {
650. return $original;
651. } else {
652. return $str;
653. }
654. }
655. }

Documentation generated on Fri, 01 Jan 2016 10:26:02 +0200 by phpDocumentor 1.4.0