1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 /** 4 ******************************************************************************* 5 * Copyright (C) 2002-2010, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9 10 11 package com.ibm.icu.dev.tool.layout; 12 13 import com.ibm.icu.lang.UCharacter; 14 import com.ibm.icu.lang.UScript; 15 import com.ibm.icu.text.UTF16; 16 import com.ibm.icu.text.UnicodeSet; 17 18 /** 19 * @author Eric Mader 20 * 21 * Notes: 22 * 23 * The property \p{Decomposition_Type=Canonical} will match all characters with a canonical 24 * decomposition. 25 * 26 * So "[[\\p{Latin}\\p{Greek}\\p{Cyrillic}] & [\\p{Decomposition_Type=Canonical}]]" 27 * will match all Latin, Greek and Cyrillic characters with a canonical decomposition. 28 * 29 * Are these three scripts enough? Do we want to collect them all at once and distribute by script, 30 * or process them one script at a time. It's probably a good idea to build a single table for 31 * however many scripts there are. 32 * 33 * It might be better to collect all the characters that have a canonical decomposition and just 34 * sort them into however many scripts there are... unless we'll get characters in COMMON??? 35 */ 36 public class CanonGSUBBuilder 37 { convertArabicString(int type, int ligature, String decomp, ClassTable isolClassTable)38 static public String convertArabicString(int type, int ligature, String decomp, ClassTable isolClassTable) 39 { 40 int leftType = ArabicShaping.VALUE_NONE; 41 int rightType = ArabicShaping.VALUE_NONE; 42 43 switch (type) { 44 case UCharacter.DecompositionType.ISOLATED: 45 break; 46 47 case UCharacter.DecompositionType.FINAL: 48 rightType = ArabicShaping.VALUE_LEFT; 49 break; 50 51 case UCharacter.DecompositionType.INITIAL: 52 leftType = ArabicShaping.VALUE_RIGHT; 53 break; 54 55 case UCharacter.DecompositionType.MEDIAL: 56 rightType = ArabicShaping.VALUE_LEFT; 57 leftType = ArabicShaping.VALUE_RIGHT; 58 break; 59 60 default: 61 return decomp + UCharacter.toString(ligature); 62 } 63 64 char[] chars = decomp.toCharArray(); 65 66 ArabicShaping.shape(chars, leftType, rightType, isolClassTable); 67 68 return new String(chars) + UCharacter.toString(ligature); 69 } 70 buildArabicContextualForms(ArabicCharacterData data, ClassTable initClassTable, ClassTable mediClassTable, ClassTable finaClassTable, ClassTable isolClassTable)71 static void buildArabicContextualForms(ArabicCharacterData data, ClassTable initClassTable, ClassTable mediClassTable, 72 ClassTable finaClassTable, ClassTable isolClassTable) 73 { 74 System.out.print("Finding Arabic contextual forms... "); 75 76 for (int i = 0; i < data.countRecords(); i += 1) { 77 ArabicCharacterData.Record record = data.getRecord(i); 78 String decomposition = record.getDecomposition(); 79 80 if (decomposition != null && decomposition.length() == 1) { 81 int contextual = record.getCodePoint(); 82 int isolated = UTF16.charAt(record.getDecomposition(), 0); 83 84 switch (record.getDecompositionType()) { 85 case UCharacter.DecompositionType.INITIAL: 86 initClassTable.addMapping(isolated, contextual); 87 break; 88 89 case UCharacter.DecompositionType.MEDIAL: 90 mediClassTable.addMapping(isolated, contextual); 91 break; 92 93 case UCharacter.DecompositionType.FINAL: 94 finaClassTable.addMapping(isolated, contextual); 95 break; 96 97 case UCharacter.DecompositionType.ISOLATED: 98 isolClassTable.addMapping(isolated, contextual); 99 break; 100 101 default: 102 // issue some error message? 103 break; 104 } 105 } 106 } 107 108 System.out.println("Done."); 109 } 110 buildArabicLigatureTree(ArabicCharacterData data, ClassTable isolClassTable)111 static LigatureTree buildArabicLigatureTree(ArabicCharacterData data, ClassTable isolClassTable) 112 { 113 LigatureTree contextualTree = new LigatureTree(); 114 int ligatureCount = 0; 115 116 System.out.print("Building Arabic ligature tree... "); 117 118 for (int i = 0; i < data.countRecords(); i += 1) { 119 ArabicCharacterData.Record record = data.getRecord(i); 120 String decomposition = record.getDecomposition(); 121 122 if (decomposition != null && decomposition.length() > 1) { 123 int ligature = record.getCodePoint(); 124 int decompType = record.getDecompositionType(); 125 126 switch (decompType) { 127 case UCharacter.DecompositionType.FINAL: 128 case UCharacter.DecompositionType.INITIAL: 129 case UCharacter.DecompositionType.MEDIAL: 130 case UCharacter.DecompositionType.ISOLATED: 131 contextualTree.insert(convertArabicString(decompType, ligature, decomposition, isolClassTable)); 132 ligatureCount += 1; 133 break; 134 135 case UCharacter.DecompositionType.CANONICAL: 136 //cannonicalTree.insert(decomposition + UCharacter.toString(ligature)); 137 break; 138 } 139 } 140 } 141 142 System.out.println(ligatureCount + " ligatures."); 143 144 return contextualTree; 145 } 146 147 static final int SIMPLE_GLYPH = 1; 148 static final int LIGATURE_GLYPH = 2; 149 static final int MARK_GLYPH = 3; 150 static final int COMPONENT_GLYPH = 4; 151 152 static final int categoryClassMap[] = { 153 0, // UNASSIGNED 154 SIMPLE_GLYPH, // UPPERCASE_LETTER 155 SIMPLE_GLYPH, // LOWERCASE_LETTER 156 SIMPLE_GLYPH, // TITLECASE_LETTER 157 SIMPLE_GLYPH, // MODIFIER_LETTER 158 SIMPLE_GLYPH, // OTHER_LETTER 159 MARK_GLYPH, // NON_SPACING_MARK 160 MARK_GLYPH, // ENCLOSING_MARK ?? 161 MARK_GLYPH, // COMBINING_SPACING_MARK ?? 162 SIMPLE_GLYPH, // DECIMAL_NUMBER 163 SIMPLE_GLYPH, // LETTER_NUMBER 164 SIMPLE_GLYPH, // OTHER_NUMBER; 165 0, // SPACE_SEPARATOR 166 0, // LINE_SEPARATOR 167 0, // PARAGRAPH_SEPARATOR 168 0, // CONTROL 169 0, // FORMAT 170 0, // PRIVATE_USE 171 0, // SURROGATE 172 SIMPLE_GLYPH, // DASH_PUNCTUATION 173 SIMPLE_GLYPH, // START_PUNCTUATION 174 SIMPLE_GLYPH, // END_PUNCTUATION 175 SIMPLE_GLYPH, // CONNECTOR_PUNCTUATION 176 SIMPLE_GLYPH, // OTHER_PUNCTUATION 177 SIMPLE_GLYPH, // MATH_SYMBOL; 178 SIMPLE_GLYPH, // CURRENCY_SYMBOL 179 SIMPLE_GLYPH, // MODIFIER_SYMBOL 180 SIMPLE_GLYPH, // OTHER_SYMBOL 181 SIMPLE_GLYPH, // INITIAL_PUNCTUATION 182 SIMPLE_GLYPH // FINAL_PUNCTUATION 183 }; 184 getGlyphClass(ArabicCharacterData.Record record)185 static int getGlyphClass(ArabicCharacterData.Record record) 186 { 187 String decomp = record.getDecomposition(); 188 189 if (decomp != null && decomp.length() > 1) { 190 return LIGATURE_GLYPH; 191 } 192 193 return categoryClassMap[record.getGeneralCategory()]; 194 } 195 addArabicGlyphClasses(ArabicCharacterData data, ClassTable classTable)196 static void addArabicGlyphClasses(ArabicCharacterData data, ClassTable classTable) 197 { 198 System.out.print("Adding Arabic glyph classes... "); 199 200 for (int i = 0; i < data.countRecords(); i += 1) { 201 ArabicCharacterData.Record record = data.getRecord(i); 202 classTable.addMapping(record.getCodePoint(), getGlyphClass(record)); 203 } 204 205 System.out.println("Done."); 206 } 207 buildArabicTables(ScriptList scriptList, FeatureList featureList, LookupList lookupList, ClassTable classTable)208 private static void buildArabicTables(ScriptList scriptList, FeatureList featureList, 209 LookupList lookupList, ClassTable classTable) { 210 // TODO: Might want to have the ligature table builder explicitly check for ligatures 211 // which start with space and tatweel rather than pulling them out here... 212 UnicodeSet arabicBlock = new UnicodeSet("[[\\p{block=Arabic}] & [[:Cf:][:Po:][:So:][:Mn:][:Nd:][:Lm:]]]"); 213 UnicodeSet oddLigatures = new UnicodeSet("[\\uFC5E-\\uFC63\\uFCF2-\\uFCF4\\uFE70-\\uFE7F]"); 214 UnicodeSet arabicLetters = new UnicodeSet("[\\p{Arabic}]"); 215 ArabicCharacterData arabicData = ArabicCharacterData.factory(arabicLetters.addAll(arabicBlock).removeAll(oddLigatures)); 216 217 addArabicGlyphClasses(arabicData, classTable); 218 219 ClassTable initClassTable = new ClassTable(); 220 ClassTable mediClassTable = new ClassTable(); 221 ClassTable finaClassTable = new ClassTable(); 222 ClassTable isolClassTable = new ClassTable(); 223 224 buildArabicContextualForms(arabicData, initClassTable, mediClassTable, finaClassTable, isolClassTable); 225 isolClassTable.snapshot(); 226 LigatureTree ligaTree = buildArabicLigatureTree(arabicData, isolClassTable); 227 228 LigatureTreeWalker ligaWalker = new LigatureTreeWalker(); 229 230 ligaTree.walk(ligaWalker); 231 232 Lookup initLookup, mediLookup, finaLookup, ligaLookup; 233 234 initLookup = new Lookup(Lookup.GSST_Single, 0); 235 initLookup.addSubtable(initClassTable); 236 237 mediLookup = new Lookup(Lookup.GSST_Single, 0); 238 mediLookup.addSubtable(mediClassTable); 239 240 finaLookup = new Lookup(Lookup.GSST_Single, 0); 241 finaLookup.addSubtable(finaClassTable); 242 243 ligaLookup = new Lookup(Lookup.GSST_Ligature, Lookup.LF_IgnoreMarks); 244 ligaLookup.addSubtable(ligaWalker); 245 246 Feature init = new Feature("init"); 247 Feature medi = new Feature("medi"); 248 Feature fina = new Feature("fina"); 249 Feature liga = new Feature("liga"); 250 251 init.addLookup(lookupList.addLookup(initLookup)); 252 medi.addLookup(lookupList.addLookup(mediLookup)); 253 fina.addLookup(lookupList.addLookup(finaLookup)); 254 liga.addLookup(lookupList.addLookup(ligaLookup)); 255 256 featureList.addFeature(init); 257 featureList.addFeature(medi); 258 featureList.addFeature(fina); 259 featureList.addFeature(liga); 260 261 scriptList.addFeature("arab", "(default)", init); 262 scriptList.addFeature("arab", "(default)", medi); 263 scriptList.addFeature("arab", "(default)", fina); 264 scriptList.addFeature("arab", "(default)", liga); 265 266 System.out.println(); 267 } 268 buildLigatureTree(CanonicalCharacterData data, int script, LigatureTree ligatureTree)269 public static void buildLigatureTree(CanonicalCharacterData data, int script, LigatureTree ligatureTree) 270 { 271 int ligatureCount = 0; 272 273 System.out.print("building composition ligature tree for " + UScript.getName(script) + "... "); 274 275 for (int i = 0; i < data.countRecords(script); i += 1) { 276 CanonicalCharacterData.Record record = data.getRecord(script, i); 277 String composed = UCharacter.toString(record.getComposedCharacter()); 278 279 for (int e = 0; e < record.countEquivalents(); e += 1) { 280 String equivalent = record.getEquivalent(e); 281 282 ligatureTree.insert(equivalent + composed); 283 ligatureCount += 1; 284 } 285 } 286 287 System.out.println(ligatureCount + " ligatures."); 288 } 289 buildDecompTables(CanonicalCharacterData data, int script)290 public static DecompTable[] buildDecompTables(CanonicalCharacterData data, int script) 291 { 292 int maxDecompCount = data.getMaxEquivalents(script); 293 DecompTable[] decompTables = new DecompTable[maxDecompCount]; 294 295 System.out.print("Building decompositon tables for " + UScript.getName(script) + 296 "... total decompositions: " + data.countRecords(script) + 297 ", max: " + maxDecompCount + "..."); 298 299 for (int i = 0; i < maxDecompCount; i += 1) { 300 DecompTable table = new DecompTable(); 301 302 for (int r = 0; r < data.countRecords(script); r += 1) { 303 CanonicalCharacterData.Record record = data.getRecord(script, r); 304 305 if (record.countEquivalents() > i) { 306 table.add(record.getComposedCharacter(), record.getEquivalent(i)); 307 } 308 } 309 310 decompTables[i] = table; 311 } 312 313 System.out.println(" Done."); 314 315 return decompTables; 316 } 317 buildLookups(CanonicalCharacterData data, LookupList lookupList, int script)318 public static int[] buildLookups(CanonicalCharacterData data, LookupList lookupList, int script) 319 { 320 int[] lookups = new int[2]; 321 322 DecompTable[] decompTables = buildDecompTables(data, script); 323 324 LigatureTree compTree = new LigatureTree(); 325 326 buildLigatureTree(data, script, compTree); 327 328 System.out.println(); 329 330 LigatureTreeWalker compWalker = new LigatureTreeWalker(); 331 332 compTree.walk(compWalker); 333 334 Lookup compLookup, dcmpLookup; 335 //int compLookupIndex, dcmpLookupIndex; 336 337 compLookup = new Lookup(Lookup.GSST_Ligature, 0); 338 compLookup.addSubtable(compWalker); 339 340 dcmpLookup = new Lookup(Lookup.GSST_Multiple, 0); 341 for (int i = 0; i < decompTables.length; i += 1) { 342 dcmpLookup.addSubtable(decompTables[i]); 343 } 344 345 lookups[0] = lookupList.addLookup(compLookup); 346 lookups[1] = lookupList.addLookup(dcmpLookup); 347 348 return lookups; 349 } 350 addLookups(Feature feature, int[] lookups)351 public static void addLookups(Feature feature, int[] lookups) 352 { 353 for (int i = 0; i < lookups.length; i += 1) { 354 feature.addLookup(lookups[i]); 355 } 356 } 357 358 /* 359 * Hebrew mark order taken from the SBL Hebrew Font manual 360 * Arabic mark order per Thomas Milo: hamza < shadda < combining_alef < sukun, vowel_marks < madda < qur'anic_marks 361 */ buildCombiningClassTable()362 public static ClassTable buildCombiningClassTable() 363 { 364 UnicodeSet markSet = new UnicodeSet("[\\P{CanonicalCombiningClass=0}]"); 365 ClassTable exceptions = new ClassTable(); 366 ClassTable combiningClasses = new ClassTable(); 367 int markCount = markSet.size(); 368 369 exceptions.addMapping(0x05C1, 10); // Point Shin Dot 370 exceptions.addMapping(0x05C2, 11); // Point Sin Dot 371 exceptions.addMapping(0x05BC, 21); // Point Dagesh or Mapiq 372 exceptions.addMapping(0x05BF, 23); // Point Rafe 373 exceptions.addMapping(0x05B9, 27); // Point Holam 374 exceptions.addMapping(0x0323, 220); // Comb. Dot Below (low punctum) 375 exceptions.addMapping(0x0591, 220); // Accent Etnahta 376 exceptions.addMapping(0x0596, 220); // Accent Tipeha 377 exceptions.addMapping(0x059B, 220); // Accent Tevir 378 exceptions.addMapping(0x05A3, 220); // Accent Munah 379 exceptions.addMapping(0x05A4, 220); // Accent Mahapakh 380 exceptions.addMapping(0x05A5, 220); // Accent Merkha 381 exceptions.addMapping(0x05A6, 220); // Accent Merkha Kefula 382 exceptions.addMapping(0x05A7, 220); // Accent Darga 383 exceptions.addMapping(0x05AA, 220); // Accent Yerah Ben Yomo 384 exceptions.addMapping(0x05B0, 220); // Point Sheva 385 exceptions.addMapping(0x05B1, 220); // Point Hataf Segol 386 exceptions.addMapping(0x05B2, 220); // Point Hataf Patah 387 exceptions.addMapping(0x05B3, 220); // Point Hataf Qamats 388 exceptions.addMapping(0x05B4, 220); // Point Hiriq 389 exceptions.addMapping(0x05B5, 220); // Point Tsere 390 exceptions.addMapping(0x05B6, 220); // Point Segol 391 exceptions.addMapping(0x05B7, 220); // Point Patah 392 exceptions.addMapping(0x05B8, 220); // Point Qamats 393 exceptions.addMapping(0x05BB, 220); // Point Qubuts 394 exceptions.addMapping(0x05BD, 220); // Point Meteg 395 exceptions.addMapping(0x059A, 222); // Accent Yetiv 396 exceptions.addMapping(0x05AD, 222); // Accent Dehi 397 exceptions.addMapping(0x05C4, 230); // Mark Upper Dot (high punctum) 398 exceptions.addMapping(0x0593, 230); // Accent Shalshelet 399 exceptions.addMapping(0x0594, 230); // Accent Zaqef Qatan 400 exceptions.addMapping(0x0595, 230); // Accent Zaqef Gadol 401 exceptions.addMapping(0x0597, 230); // Accent Revia 402 exceptions.addMapping(0x0598, 230); // Accent Zarqa 403 exceptions.addMapping(0x059F, 230); // Accent Qarney Para 404 exceptions.addMapping(0x059E, 230); // Accent Gershayim 405 exceptions.addMapping(0x059D, 230); // Accent Geresh Muqdam 406 exceptions.addMapping(0x059C, 230); // Accent Geresh 407 exceptions.addMapping(0x0592, 230); // Accent Segolta 408 exceptions.addMapping(0x05A0, 230); // Accent Telisha Gedola 409 exceptions.addMapping(0x05AC, 230); // Accent Iluy 410 exceptions.addMapping(0x05A8, 230); // Accent Qadma 411 exceptions.addMapping(0x05AB, 230); // Accent Ole 412 exceptions.addMapping(0x05AF, 230); // Mark Masora Circle 413 exceptions.addMapping(0x05A1, 230); // Accent Pazer 414 //exceptions.addMapping(0x0307, 230); // Mark Number/Masora Dot 415 exceptions.addMapping(0x05AE, 232); // Accent Zinor 416 exceptions.addMapping(0x05A9, 232); // Accent Telisha Qetana 417 exceptions.addMapping(0x0599, 232); // Accent Pashta 418 419 exceptions.addMapping(0x0655, 27); // ARABIC HAMZA BELOW 420 exceptions.addMapping(0x0654, 27); // ARABIC HAMZA ABOVE 421 422 exceptions.addMapping(0x0651, 28); // ARABIC SHADDA 423 424 exceptions.addMapping(0x0656, 29); // ARABIC SUBSCRIPT ALEF 425 exceptions.addMapping(0x0670, 29); // ARABIC LETTER SUPERSCRIPT ALEF 426 427 exceptions.addMapping(0x064D, 30); // ARABIC KASRATAN 428 exceptions.addMapping(0x0650, 30); // ARABIC KASRA 429 430 exceptions.addMapping(0x0652, 31); // ARABIC SUKUN 431 exceptions.addMapping(0x06E1, 31); // ARABIC SMALL HIGH DOTLESS HEAD OF KHAH 432 433 exceptions.addMapping(0x064B, 31); // ARABIC FATHATAN 434 exceptions.addMapping(0x064C, 31); // ARABIC DAMMATAN 435 exceptions.addMapping(0x064E, 31); // ARABIC FATHA 436 exceptions.addMapping(0x064F, 31); // ARABIC DAMMA 437 exceptions.addMapping(0x0657, 31); // ARABIC INVERTED DAMMA 438 exceptions.addMapping(0x0658, 31); // ARABIC MARK NOON GHUNNA 439 440 exceptions.addMapping(0x0653, 32); // ARABIC MADDAH ABOVE 441 442 exceptions.snapshot(); 443 444 for (int i = 0; i < markCount; i += 1) { 445 int mark = markSet.charAt(i); 446 int markClass = exceptions.getGlyphClassID(mark); 447 448 if (markClass == 0) { 449 markClass = UCharacter.getCombiningClass(mark); 450 } 451 452 combiningClasses.addMapping(mark, markClass); 453 } 454 455 combiningClasses.snapshot(); 456 return combiningClasses; 457 } 458 buildDecompTables(String fileName)459 public static void buildDecompTables(String fileName) 460 { 461 // F900 - FAFF are compatibility ideographs. They all decompose to a single other character, and can be ignored. 462 //UnicodeSet decompSet = new UnicodeSet("[[[\\P{Hangul}] & [\\p{DecompositionType=Canonical}]] - [\uF900-\uFAFF]]"); 463 UnicodeSet decompSet = new UnicodeSet("[[\\p{DecompositionType=Canonical}] & [\\P{FullCompositionExclusion}] & [\\P{Hangul}]]"); 464 CanonicalCharacterData data = CanonicalCharacterData.factory(decompSet); 465 ClassTable classTable = new ClassTable(); 466 467 LookupList lookupList = new LookupList(); 468 FeatureList featureList = new FeatureList(); 469 ScriptList scriptList = new ScriptList(); 470 471 // build common, inherited lookups... 472 // int[] commonLookups = buildLookups(data, lookupList, UScript.COMMON); 473 // int[] inheritedLookups = buildLookups(data, lookupList, UScript.INHERITED); 474 475 for (int script = 0; script < UScript.CODE_LIMIT; script += 1) { 476 477 // This is a bit lame, but it's the only way I can think of 478 // to make this work w/o knowing the values of COMMON and INHERITED... 479 if (script == UScript.COMMON || script == UScript.INHERITED || 480 data.getMaxEquivalents(script) == 0) { 481 continue; 482 } 483 484 int[] lookups = buildLookups(data, lookupList, script); 485 486 Feature ccmp = new Feature("ccmp"); 487 488 addLookups(ccmp, lookups); 489 // addLookups(ccmp, commonLookups); 490 // addLookups(ccmp, inheritedLookups); 491 492 featureList.addFeature(ccmp); 493 494 String scriptTag = TagUtilities.tagLabel(UScript.getShortName(script)); 495 496 scriptList.addFeature(scriptTag, "(default)", ccmp); 497 498 if (script == UScript.ARABIC) { 499 buildArabicTables(scriptList, featureList, lookupList, classTable); 500 } 501 } 502 503 featureList.finalizeFeatureList(); 504 505 ClassTable markClassTable = buildCombiningClassTable(); 506 507 GSUBWriter gsubWriter = new GSUBWriter("Canon", scriptList, featureList, lookupList); 508 GDEFWriter gdefWriter = new GDEFWriter("Canon", classTable, markClassTable); 509 String[] includeFiles = {"LETypes.h", "CanonShaping.h"}; 510 511 LigatureModuleWriter writer = new LigatureModuleWriter(); 512 513 writer.openFile(fileName); 514 writer.writeHeader(null, includeFiles); 515 writer.writeTable(gsubWriter); 516 writer.writeTable(gdefWriter); 517 writer.writeTrailer(); 518 writer.closeFile(); 519 } 520 main(String[] args)521 public static void main(String[] args) 522 { 523 buildDecompTables(args[0]); 524 } 525 } 526