001: /*
002:
003: Licensed to the Apache Software Foundation (ASF) under one or more
004: contributor license agreements. See the NOTICE file distributed with
005: this work for additional information regarding copyright ownership.
006: The ASF licenses this file to You under the Apache License, Version 2.0
007: (the "License"); you may not use this file except in compliance with
008: the License. You may obtain a copy of the License at
009:
010: http://www.apache.org/licenses/LICENSE-2.0
011:
012: Unless required by applicable law or agreed to in writing, software
013: distributed under the License is distributed on an "AS IS" BASIS,
014: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: See the License for the specific language governing permissions and
016: limitations under the License.
017:
018: */
019: package org.apache.batik.gvt.text;
020:
021: import java.text.AttributedCharacterIterator;
022: import java.text.AttributedString;
023: import java.util.Map;
024:
025: /**
026: * Handles the processing of arabic text. In particular it determines the
027: * form each arabic char should take. It also contains methods for substituting
028: * plain arabic glyphs with their shaped forms. This is needed when the arabic
029: * text is rendered using an AWT font.
030: *
031: * @author <a href="mailto:bella.robinson@cmis.csiro.au">Bella Robinson</a>
032: * @version $Id: ArabicTextHandler.java 491229 2006-12-30 14:37:28Z dvholten $
033: */
034: public class ArabicTextHandler {
035:
036: private static final int arabicStart = 0x0600;
037: private static final int arabicEnd = 0x06FF;
038:
039: private static final AttributedCharacterIterator.Attribute ARABIC_FORM = GVTAttributedCharacterIterator.TextAttribute.ARABIC_FORM;
040: private static final Integer ARABIC_NONE = GVTAttributedCharacterIterator.TextAttribute.ARABIC_NONE;
041: private static final Integer ARABIC_ISOLATED = GVTAttributedCharacterIterator.TextAttribute.ARABIC_ISOLATED;
042: private static final Integer ARABIC_TERMINAL = GVTAttributedCharacterIterator.TextAttribute.ARABIC_TERMINAL;
043: private static final Integer ARABIC_INITIAL = GVTAttributedCharacterIterator.TextAttribute.ARABIC_INITIAL;
044: private static final Integer ARABIC_MEDIAL = GVTAttributedCharacterIterator.TextAttribute.ARABIC_MEDIAL;
045:
046: /**
047: * private ctor prevents unnecessary instantiation of this class.
048: */
049: private ArabicTextHandler() {
050:
051: }
052:
053: /**
054: * If the AttributedString contains any arabic chars, assigns an
055: * arabic form attribute, i.e. initial|medial|terminal|isolated,
056: * to each arabic char.
057: *
058: * @param as The string to attach the arabic form attributes to.
059: * @return An attributed string with arabic form attributes.
060: */
061: public static AttributedString assignArabicForms(AttributedString as) {
062:
063: // first check to see if the string contains any arabic chars
064: // if not, then don't need to do anything
065: if (!containsArabic(as)) {
066: return as;
067: }
068:
069: // if the string contains any ligatures with transparent chars
070: // eg. AtB where AB form a ligature and t is transparent, then
071: // reorder that part of the string so that it becomes tAB
072: // construct the reordered ACI
073: AttributedCharacterIterator aci = as.getIterator();
074: int numChars = aci.getEndIndex() - aci.getBeginIndex();
075: int[] charOrder = null;
076: if (numChars >= 3) {
077: char prevChar = aci.first();
078: char c = aci.next();
079: int i = 1;
080: for (char nextChar = aci.next(); nextChar != AttributedCharacterIterator.DONE; prevChar = c, c = nextChar, nextChar = aci
081: .next(), i++) {
082: if (arabicCharTransparent(c)) {
083: if (hasSubstitute(prevChar, nextChar)) {
084: // found a ligature, separated by a transparent char
085: if (charOrder == null) {
086: charOrder = new int[numChars];
087: for (int j = 0; j < numChars; j++) {
088: charOrder[j] = j + aci.getBeginIndex();
089: }
090: }
091: int temp = charOrder[i];
092: charOrder[i] = charOrder[i - 1];
093: charOrder[i - 1] = temp;
094: }
095: }
096: }
097: }
098:
099: if (charOrder != null) {
100: // need to reconstruct the reordered attributed string
101: StringBuffer reorderedString = new StringBuffer(numChars);
102: char c;
103: for (int i = 0; i < numChars; i++) {
104: c = aci.setIndex(charOrder[i]);
105: reorderedString.append(c);
106: }
107: AttributedString reorderedAS;
108: reorderedAS = new AttributedString(reorderedString
109: .toString());
110:
111: for (int i = 0; i < numChars; i++) {
112: aci.setIndex(charOrder[i]);
113: Map attributes = aci.getAttributes();
114: reorderedAS.addAttributes(attributes, i, i + 1);
115: }
116:
117: if (charOrder[0] != aci.getBeginIndex()) {
118: // have swapped the first char. Need to move
119: // any position attributes
120:
121: aci.setIndex(charOrder[0]);
122: Float x = (Float) aci
123: .getAttribute(GVTAttributedCharacterIterator.TextAttribute.X);
124: Float y = (Float) aci
125: .getAttribute(GVTAttributedCharacterIterator.TextAttribute.Y);
126:
127: if (x != null && !x.isNaN()) {
128: reorderedAS
129: .addAttribute(
130: GVTAttributedCharacterIterator.TextAttribute.X,
131: new Float(Float.NaN), charOrder[0],
132: charOrder[0] + 1);
133: reorderedAS
134: .addAttribute(
135: GVTAttributedCharacterIterator.TextAttribute.X,
136: x, 0, 1);
137: }
138: if (y != null && !y.isNaN()) {
139: reorderedAS
140: .addAttribute(
141: GVTAttributedCharacterIterator.TextAttribute.Y,
142: new Float(Float.NaN), charOrder[0],
143: charOrder[0] + 1);
144: reorderedAS
145: .addAttribute(
146: GVTAttributedCharacterIterator.TextAttribute.Y,
147: y, 0, 1);
148: }
149: }
150: as = reorderedAS;
151: }
152:
153: // first assign none to all arabic letters
154: aci = as.getIterator();
155: int runStart = -1;
156: int idx = aci.getBeginIndex();
157: for (int c = aci.first(); c != AttributedCharacterIterator.DONE; c = aci
158: .next(), idx++) {
159: if ((c >= arabicStart) && (c <= arabicEnd)) {
160: if (runStart == -1)
161: runStart = idx;
162: } else if (runStart != -1) {
163: as
164: .addAttribute(ARABIC_FORM, ARABIC_NONE,
165: runStart, idx);
166: runStart = -1;
167: }
168: }
169: if (runStart != -1)
170: as.addAttribute(ARABIC_FORM, ARABIC_NONE, runStart, idx);
171:
172: aci = as.getIterator(); // Make sure ACI tracks ARABIC_FORM
173: int end = aci.getBeginIndex();
174:
175: Integer currentForm = ARABIC_NONE;
176: // for each run of arabic chars, assign the appropriate form
177: while (aci.setIndex(end) != AttributedCharacterIterator.DONE) {
178: int start = aci.getRunStart(ARABIC_FORM);
179: end = aci.getRunLimit(ARABIC_FORM);
180: char currentChar = aci.setIndex(start);
181: currentForm = (Integer) aci.getAttribute(ARABIC_FORM);
182:
183: if (currentForm == null) {
184: // only modify if the chars in the run are arabic
185: continue;
186: }
187:
188: int currentIndex = start;
189: int prevCharIndex = start - 1;
190: while (currentIndex < end) {
191: char prevChar = currentChar;
192: currentChar = aci.setIndex(currentIndex);
193: while (arabicCharTransparent(currentChar)
194: && (currentIndex < end)) {
195: currentIndex++;
196: currentChar = aci.setIndex(currentIndex);
197: }
198: if (currentIndex >= end) {
199: break;
200: }
201:
202: Integer prevForm = currentForm;
203: currentForm = ARABIC_NONE;
204: if (prevCharIndex >= start) { // if not at the start
205: // if prev char right AND current char left
206: if (arabicCharShapesRight(prevChar)
207: && arabicCharShapesLeft(currentChar)) {
208: // Increment the form of the previous char
209: prevForm = new Integer(prevForm.intValue() + 1);
210: as.addAttribute(ARABIC_FORM, prevForm,
211: prevCharIndex, prevCharIndex + 1);
212:
213: // and set the form of the current char to INITIAL
214: currentForm = ARABIC_INITIAL;
215: } else if (arabicCharShaped(currentChar)) {
216: // set the form of the current char to ISOLATE
217: currentForm = ARABIC_ISOLATED;
218: }
219:
220: // if this is the first arabic char and its
221: // shaped, set to ISOLATE
222: } else if (arabicCharShaped(currentChar)) {
223: // set the form of the current char to ISOLATE
224: currentForm = ARABIC_ISOLATED;
225: }
226: if (currentForm != ARABIC_NONE)
227: as.addAttribute(ARABIC_FORM, currentForm,
228: currentIndex, currentIndex + 1);
229: prevCharIndex = currentIndex;
230: currentIndex++;
231: }
232: }
233: return as;
234: }
235:
236: /**
237: * Returns true if the char is a standard arabic char.
238: * (ie. within the range U+0600 - U+6FF)
239: *
240: * @param c The character to test.
241: * @return True if the char is arabic, false otherwise.
242: */
243: public static boolean arabicChar(char c) {
244: if (c >= arabicStart && c <= arabicEnd) {
245: return true;
246: }
247: return false;
248: }
249:
250: /**
251: * Returns true if the string contains any arabic characters.
252: *
253: * @param as The string to test.
254: * @return True if at least one char is arabic, false otherwise.
255: */
256: public static boolean containsArabic(AttributedString as) {
257: return containsArabic(as.getIterator());
258: }
259:
260: /**
261: * Returns true if the ACI contains any arabic characters.
262: *
263: * @param aci The AttributedCharacterIterator to test.
264: * @return True if at least one char is arabic, false otherwise.
265: */
266: public static boolean containsArabic(AttributedCharacterIterator aci) {
267: for (char c = aci.first(); c != AttributedCharacterIterator.DONE; c = aci
268: .next()) {
269: if (arabicChar(c)) {
270: return true;
271: }
272: }
273: return false;
274: }
275:
276: /**
277: * Returns true if the char is transparent.
278: *
279: * @param c The character to test.
280: * @return True if the character is transparent, false otherwise.
281: */
282: public static boolean arabicCharTransparent(char c) {
283: int charVal = c;
284: if ((charVal < 0x064B) || (charVal > 0x06ED))
285: return false;
286:
287: if ((charVal <= 0x0655) || (charVal == 0x0670)
288: || (charVal >= 0x06D6 && charVal <= 0x06E4)
289: || (charVal >= 0x06E7 && charVal <= 0x06E8)
290: || (charVal >= 0x06EA)) {
291: return true;
292: }
293: return false;
294: }
295:
296: /**
297: * Returns true if the character shapes to the right. Note that duel
298: * shaping characters also shape to the right and so will return true.
299: *
300: * @param c The character to test.
301: * @return True if the character shapes to the right, false otherwise.
302: */
303: private static boolean arabicCharShapesRight(char c) {
304: int charVal = c;
305: if ((charVal >= 0x0622 && charVal <= 0x0625)
306: || (charVal == 0x0627) || (charVal == 0x0629)
307: || (charVal >= 0x062F && charVal <= 0x0632)
308: || (charVal == 0x0648)
309: || (charVal >= 0x0671 && charVal <= 0x0673)
310: || (charVal >= 0x0675 && charVal <= 0x0677)
311: || (charVal >= 0x0688 && charVal <= 0x0699)
312: || (charVal == 0x06C0)
313: || (charVal >= 0x06C2 && charVal <= 0x06CB)
314: || (charVal == 0x06CD) || (charVal == 0x06CF)
315: || (charVal >= 0x06D2 && charVal <= 0x06D3)
316: // check for duel shaping too
317: || arabicCharShapesDuel(c)) {
318: return true;
319: }
320: return false;
321: }
322:
323: /**
324: * Returns true if character has duel shaping.
325: *
326: * @param c The character to test.
327: * @return True if the character is duel shaping, false otherwise.
328: */
329: private static boolean arabicCharShapesDuel(char c) {
330: int charVal = c;
331:
332: if ((charVal == 0x0626) || (charVal == 0x0628)
333: || (charVal >= 0x062A && charVal <= 0x062E)
334: || (charVal >= 0x0633 && charVal <= 0x063A)
335: || (charVal >= 0x0641 && charVal <= 0x0647)
336: || (charVal >= 0x0649 && charVal <= 0x064A)
337: || (charVal >= 0x0678 && charVal <= 0x0687)
338: || (charVal >= 0x069A && charVal <= 0x06BF)
339: || (charVal == 0x6C1) || (charVal == 0x6CC)
340: || (charVal == 0x6CE)
341: || (charVal >= 0x06D0 && charVal <= 0x06D1)
342: || (charVal >= 0x06FA && charVal <= 0x06FC)) {
343: return true;
344: }
345: return false;
346: }
347:
348: /**
349: * Returns true if character shapes to the left. Note that duel
350: * shaping characters also shape to the left and so will return true.
351: *
352: * @param c The character to test.
353: * @return True if the character shapes to the left, false otherwise.
354: */
355: private static boolean arabicCharShapesLeft(char c) {
356: return arabicCharShapesDuel(c);
357: }
358:
359: /**
360: * Returns true if character is shaped.
361: *
362: * @param c The character to test.
363: * @return True if the character is shaped, false otherwise.
364: */
365: private static boolean arabicCharShaped(char c) {
366: return arabicCharShapesRight(c);
367: }
368:
369: public static boolean hasSubstitute(char ch1, char ch2) {
370: if ((ch1 < doubleCharFirst) || (ch1 > doubleCharLast))
371: return false;
372:
373: int[][] remaps = doubleCharRemappings[ch1 - doubleCharFirst];
374: if (remaps == null)
375: return false;
376: for (int i = 0; i < remaps.length; i++) {
377: if (remaps[i][0] == ch2)
378: return true;
379: }
380: return false;
381: }
382:
383: /**
384: * Will try and find a substitute character of the specified form.
385: *
386: * @param ch1 The first character of two to replace.
387: * @param ch2 The second character of two to replace.
388: * @param form Indicates the required arabic form.
389: * (isolated = 1, final = 2, initial = 3, medial = 4)
390: *
391: * @return The unicode value of the substutute char, or -1 if no substitute
392: * exists.
393: */
394: public static int getSubstituteChar(char ch1, char ch2, int form) {
395: if (form == 0)
396: return -1;
397: if ((ch1 < doubleCharFirst) || (ch1 > doubleCharLast))
398: return -1;
399:
400: int[][] remaps = doubleCharRemappings[ch1 - doubleCharFirst];
401: if (remaps == null)
402: return -1;
403: for (int i = 0; i < remaps.length; i++) {
404: if (remaps[i][0] == ch2)
405: return remaps[i][form];
406: }
407: return -1;
408: }
409:
410: public static int getSubstituteChar(char ch, int form) {
411: if (form == 0)
412: return -1;
413: if ((ch < singleCharFirst) || (ch > singleCharLast))
414: return -1;
415:
416: int[] chars = singleCharRemappings[ch - singleCharFirst];
417: if (chars == null)
418: return -1;
419:
420: return chars[form - 1];
421: }
422:
423: /**
424: * Where possible substitues plain arabic glyphs with their shaped
425: * forms. This is needed when the arabic text is rendered using
426: * an AWT font. Simple arabic ligatures will also be recognised
427: * and replaced by a single character so the length of the
428: * resulting string may be shorter than the number of characters
429: * in the aci.
430: *
431: * @param aci Contains the text to process. Arabic form attributes
432: * should already be assigned to each arabic character.
433: * @return A String containing the shaped versions of the arabic characters
434: */
435: public static String createSubstituteString(
436: AttributedCharacterIterator aci) {
437: int start = aci.getBeginIndex();
438: int end = aci.getEndIndex();
439: int numChar = end - start;
440: StringBuffer substString = new StringBuffer(numChar);
441: for (int i = start; i < end; i++) {
442: char c = aci.setIndex(i);
443: if (!arabicChar(c)) {
444: substString.append(c);
445: continue;
446: }
447:
448: Integer form = (Integer) aci.getAttribute(ARABIC_FORM);
449: // see if the c is the start of a ligature
450: if (charStartsLigature(c) && (i + 1 < end)) {
451: char nextChar = aci.setIndex(i + 1);
452: Integer nextForm = (Integer) aci
453: .getAttribute(ARABIC_FORM);
454: if (form != null && nextForm != null) {
455: if (form.equals(ARABIC_TERMINAL)
456: && nextForm.equals(ARABIC_INITIAL)) {
457: // look for an isolated ligature
458: int substChar = ArabicTextHandler
459: .getSubstituteChar(c, nextChar,
460: ARABIC_ISOLATED.intValue());
461: if (substChar > -1) {
462: substString.append((char) substChar);
463: i++;
464: continue;
465: }
466: } else if (form.equals(ARABIC_TERMINAL)) {
467: // look for a terminal ligature
468: int substChar = ArabicTextHandler
469: .getSubstituteChar(c, nextChar,
470: ARABIC_TERMINAL.intValue());
471: if (substChar > -1) {
472: substString.append((char) substChar);
473: i++;
474: continue;
475: }
476: } else if (form.equals(ARABIC_MEDIAL)
477: && nextForm.equals(ARABIC_MEDIAL)) {
478: // look for a medial ligature
479: int substChar = ArabicTextHandler
480: .getSubstituteChar(c, nextChar,
481: ARABIC_MEDIAL.intValue());
482: if (substChar > -1) {
483: substString.append((char) substChar);
484: i++;
485: continue;
486: }
487: }
488: }
489: }
490:
491: // couldn't find a matching ligature so just look for a
492: // simple substitution
493: if (form != null && form.intValue() > 0) {
494: int substChar = getSubstituteChar(c, form.intValue());
495: if (substChar > -1) {
496: c = (char) substChar;
497: }
498: }
499: substString.append(c);
500: }
501:
502: return substString.toString();
503: }
504:
505: /**
506: * Returns true if a ligature exists that starts with the
507: * specified character.
508: *
509: * @param c The character to test.
510: * @return True if there is a ligature that starts with c, false otherwise.
511: */
512: public static boolean charStartsLigature(char c) {
513: int charVal = c;
514: if (charVal == 0x064B || charVal == 0x064C || charVal == 0x064D
515: || charVal == 0x064E || charVal == 0x064F
516: || charVal == 0x0650 || charVal == 0x0651
517: || charVal == 0x0652 || charVal == 0x0622
518: || charVal == 0x0623 || charVal == 0x0625
519: || charVal == 0x0627) {
520: return true;
521: }
522: return false;
523: }
524:
525: /**
526: * Returns the number of characters the glyph for the specified
527: * character represents. If the glyph represents a ligature this
528: * will be 2, otherwise 1.
529: *
530: * @param c The character to test.
531: * @return The number of characters the glyph for c represents.
532: */
533: public static int getNumChars(char c) {
534: // if c is a ligature returns 2, else returns 1
535: if (isLigature(c))
536: // at the moment only support ligatures with two chars
537: return 2;
538: return 1;
539: }
540:
541: /**
542: * Returns true if the glyph for the specified character
543: * respresents a ligature.
544: *
545: * @param c The character to test.
546: * @return True if c is a ligature, false otherwise.
547: */
548: public static boolean isLigature(char c) {
549: int charVal = c;
550: if ((charVal < 0xFE70) || (charVal > 0xFEFC))
551: return false;
552:
553: if ((charVal <= 0xFE72) || (charVal == 0xFE74)
554: || (charVal >= 0xFE76 && charVal <= 0xFE7F)
555: || (charVal >= 0xFEF5)) {
556: return true;
557: }
558: return false;
559: }
560:
561: // constructs the character map that maps arabic characters and
562: // ligature to their various forms
563: // NOTE: the unicode values for ligatures are stored here in
564: // visual order (not logical order)
565:
566: // Single char remappings:
567: static int singleCharFirst = 0x0621;
568: static int singleCharLast = 0x064A;
569: static int[][] singleCharRemappings = {
570: // isolated, final, initial, medial
571: { 0xFE80, -1, -1, -1 }, // 0x0621
572: { 0xFE81, 0xFE82, -1, -1 }, // 0x0622
573: { 0xFE83, 0xFE84, -1, -1 }, // 0x0623
574: { 0xFE85, 0xFE86, -1, -1 }, // 0x0624
575: { 0xFE87, 0xFE88, -1, -1 }, // 0x0625
576: { 0xFE89, 0xFE8A, 0xFE8B, 0xFE8C }, // 0x0626
577: { 0xFE8D, 0xFE8E, -1, -1 }, // 0x0627
578: { 0xFE8F, 0xFE90, 0xFE91, 0xFE92 }, // 0x0628
579: { 0xFE93, 0xFE94, -1, -1 }, // 0x0629
580: { 0xFE95, 0xFE96, 0xFE97, 0xFE98 }, // 0x062A
581: { 0xFE99, 0xFE9A, 0xFE9B, 0xFE9C }, // 0x062B
582: { 0xFE9D, 0xFE9E, 0xFE9F, 0xFEA0 }, // 0x062C
583: { 0xFEA1, 0xFEA2, 0xFEA3, 0xFEA4 }, // 0x062D
584: { 0xFEA5, 0xFEA6, 0xFEA7, 0xFEA8 }, // 0x062E
585: { 0xFEA9, 0xFEAA, -1, -1 }, // 0x062F
586: { 0xFEAB, 0xFEAC, -1, -1 }, // 0x0630
587: { 0xFEAD, 0xFEAE, -1, -1 }, // 0x0631
588: { 0xFEAF, 0xFEB0, -1, -1 }, // 0x0632
589: { 0xFEB1, 0xFEB2, 0xFEB3, 0xFEB4 }, // 0x0633
590: { 0xFEB5, 0xFEB6, 0xFEB7, 0xFEB8 }, // 0x0634
591: { 0xFEB9, 0xFEBA, 0xFEBB, 0xFEBC }, // 0x0635
592: { 0xFEBD, 0xFEBE, 0xFEBF, 0xFEC0 }, // 0x0636
593: { 0xFEC1, 0xFEC2, 0xFEC3, 0xFEC4 }, // 0x0637
594: { 0xFEC5, 0xFEC6, 0xFEC7, 0xFEC8 }, // 0x0638
595: { 0xFEC9, 0xFECA, 0xFECB, 0xFECC }, // 0x0639
596: { 0xFECD, 0xFECE, 0xFECF, 0xFED0 }, // 0x063A
597:
598: null, // 0x063B
599: null, // 0x063C
600: null, // 0x063D
601: null, // 0x063E
602: null, // 0x063F
603: null, // 0x0640
604:
605: { 0xFED1, 0xFED2, 0xFED3, 0xFED4 }, // 0x0641
606: { 0xFED5, 0xFED6, 0xFED7, 0xFED8 }, // 0x0642
607: { 0xFED9, 0xFEDA, 0xFEDB, 0xFEDC }, // 0x0643
608: { 0xFEDD, 0xFEDE, 0xFEDF, 0xFEE0 }, // 0x0644
609: { 0xFEE1, 0xFEE2, 0xFEE3, 0xFEE4 }, // 0x0645
610: { 0xFEE5, 0xFEE6, 0xFEE7, 0xFEE8 }, // 0x0646
611: { 0xFEE9, 0xFEEA, 0xFEEB, 0xFEEC }, // 0x0647
612: { 0xFEED, 0xFEEE, -1, -1 }, // 0x0648
613: { 0xFEEF, 0xFEF0, -1, -1 }, // 0x0649
614: { 0xFEF1, 0xFEF2, 0xFEF3, 0xFEF4 } }; // 0x064A
615:
616: static int doubleCharFirst = 0x0622;
617: static int doubleCharLast = 0x0652;
618: static int[][][] doubleCharRemappings = {
619: // 2nd Char, isolated, final, initial, medial
620: { { 0x0644, 0xFEF5, 0xFEF6, -1, -1 } }, // 0x0622
621: { { 0x0644, 0xFEF7, 0xFEF8, -1, -1 } }, // 0x0623
622: null, // 0x0624
623: { { 0x0644, 0xFEF9, 0xFEFA, -1, -1 } }, // 0x0625
624: null, // 0x0626
625: { { 0x0644, 0xFEFB, 0xFEFC, -1, -1 } }, // 0x0627
626:
627: null, // 0x0628
628: null, // 0x0629
629: null, // 0x0630
630: null, // 0x0631
631: null, // 0x0632
632: null, // 0x0633
633: null, // 0x0634
634: null, // 0x0635
635: null, // 0x0636
636: null, // 0x0637
637: null, // 0x0638
638: null, // 0x0639
639: null, // 0x063A
640: null, // 0x063B
641: null, // 0x063C
642: null, // 0x063D
643: null, // 0x063E
644: null, // 0x063F
645: null, // 0x0640
646: null, // 0x0641
647: null, // 0x0642
648: null, // 0x0643
649: null, // 0x0644
650: null, // 0x0645
651: null, // 0x0646
652: null, // 0x0647
653: null, // 0x0648
654: null, // 0x0649
655: null, // 0x064A
656:
657: { { 0x0020, 0xFE70, -1, -1, -1 }, // 0x064B
658: { 0x0640, -1, -1, -1, 0xFE71 } },
659: { { 0x0020, 0xFE72, -1, -1, -1 } }, // 0x064C
660: { { 0x0020, 0xFE74, -1, -1, -1 } }, // 0x064D
661: { { 0x0020, 0xFE76, -1, -1, -1 }, // 0x064E
662: { 0x0640, -1, -1, -1, 0xFE77 } },
663: { { 0x0020, 0xFE78, -1, -1, -1 }, // 0x064F
664: { 0x0640, -1, -1, -1, 0xFE79 } },
665: { { 0x0020, 0xFE7A, -1, -1, -1 }, // 0x0650
666: { 0x0640, -1, -1, -1, 0xFE7B } },
667: { { 0x0020, 0xFE7C, -1, -1, -1 }, // 0x0651
668: { 0x0640, -1, -1, -1, 0xFE7D } },
669: { { 0x0020, 0xFE7E, -1, -1, -1 }, // 0x0652
670: { 0x0640, -1, -1, -1, 0xFE7F } } };
671: }
|