fontchain_lint.py revision 033b2226babcaeeb28cc08de6e2c2304a581bd9f
1#!/usr/bin/env python 2 3import collections 4import copy 5import glob 6import itertools 7from os import path 8import sys 9from xml.etree import ElementTree 10 11from fontTools import ttLib 12 13EMOJI_VS = 0xFE0F 14 15LANG_TO_SCRIPT = { 16 'as': 'Beng', 17 'bg': 'Cyrl', 18 'bn': 'Beng', 19 'cu': 'Cyrl', 20 'cy': 'Latn', 21 'da': 'Latn', 22 'de': 'Latn', 23 'en': 'Latn', 24 'es': 'Latn', 25 'et': 'Latn', 26 'eu': 'Latn', 27 'fr': 'Latn', 28 'ga': 'Latn', 29 'gu': 'Gujr', 30 'hi': 'Deva', 31 'hr': 'Latn', 32 'hu': 'Latn', 33 'hy': 'Armn', 34 'ja': 'Jpan', 35 'kn': 'Knda', 36 'ko': 'Kore', 37 'ml': 'Mlym', 38 'mn': 'Cyrl', 39 'mr': 'Deva', 40 'nb': 'Latn', 41 'nn': 'Latn', 42 'or': 'Orya', 43 'pa': 'Guru', 44 'pt': 'Latn', 45 'sl': 'Latn', 46 'ta': 'Taml', 47 'te': 'Telu', 48 'tk': 'Latn', 49} 50 51def lang_to_script(lang_code): 52 lang = lang_code.lower() 53 while lang not in LANG_TO_SCRIPT: 54 hyphen_idx = lang.rfind('-') 55 assert hyphen_idx != -1, ( 56 'We do not know what script the "%s" language is written in.' 57 % lang_code) 58 assumed_script = lang[hyphen_idx+1:] 59 if len(assumed_script) == 4 and assumed_script.isalpha(): 60 # This is actually the script 61 return assumed_script.title() 62 lang = lang[:hyphen_idx] 63 return LANG_TO_SCRIPT[lang] 64 65 66def printable(inp): 67 if type(inp) is set: # set of character sequences 68 return '{' + ', '.join([printable(seq) for seq in inp]) + '}' 69 if type(inp) is tuple: # character sequence 70 return '<' + (', '.join([printable(ch) for ch in inp])) + '>' 71 else: # single character 72 return 'U+%04X' % inp 73 74 75def open_font(font): 76 font_file, index = font 77 font_path = path.join(_fonts_dir, font_file) 78 if index is not None: 79 return ttLib.TTFont(font_path, fontNumber=index) 80 else: 81 return ttLib.TTFont(font_path) 82 83 84def get_best_cmap(font): 85 ttfont = open_font(font) 86 all_unicode_cmap = None 87 bmp_cmap = None 88 for cmap in ttfont['cmap'].tables: 89 specifier = (cmap.format, cmap.platformID, cmap.platEncID) 90 if specifier == (4, 3, 1): 91 assert bmp_cmap is None, 'More than one BMP cmap in %s' % (font, ) 92 bmp_cmap = cmap 93 elif specifier == (12, 3, 10): 94 assert all_unicode_cmap is None, ( 95 'More than one UCS-4 cmap in %s' % (font, )) 96 all_unicode_cmap = cmap 97 98 return all_unicode_cmap.cmap if all_unicode_cmap else bmp_cmap.cmap 99 100 101def get_variation_sequences_cmap(font): 102 ttfont = open_font(font) 103 vs_cmap = None 104 for cmap in ttfont['cmap'].tables: 105 specifier = (cmap.format, cmap.platformID, cmap.platEncID) 106 if specifier == (14, 0, 5): 107 assert vs_cmap is None, 'More than one VS cmap in %s' % (font, ) 108 vs_cmap = cmap 109 return vs_cmap 110 111 112def get_emoji_map(font): 113 # Add normal characters 114 emoji_map = copy.copy(get_best_cmap(font)) 115 reverse_cmap = {glyph: code for code, glyph in emoji_map.items()} 116 117 # Add variation sequences 118 vs_dict = get_variation_sequences_cmap(font).uvsDict 119 for vs in vs_dict: 120 for base, glyph in vs_dict[vs]: 121 if glyph is None: 122 emoji_map[(base, vs)] = emoji_map[base] 123 else: 124 emoji_map[(base, vs)] = glyph 125 126 # Add GSUB rules 127 ttfont = open_font(font) 128 for lookup in ttfont['GSUB'].table.LookupList.Lookup: 129 assert lookup.LookupType == 4, 'We only understand type 4 lookups' 130 for subtable in lookup.SubTable: 131 ligatures = subtable.ligatures 132 for first_glyph in ligatures: 133 for ligature in ligatures[first_glyph]: 134 sequence = [first_glyph] + ligature.Component 135 sequence = [reverse_cmap[glyph] for glyph in sequence] 136 sequence = tuple(sequence) 137 # Make sure no starting subsequence of 'sequence' has been 138 # seen before. 139 for sub_len in range(2, len(sequence)+1): 140 subsequence = sequence[:sub_len] 141 assert subsequence not in emoji_map 142 emoji_map[sequence] = ligature.LigGlyph 143 144 return emoji_map 145 146 147def assert_font_supports_any_of_chars(font, chars): 148 best_cmap = get_best_cmap(font) 149 for char in chars: 150 if char in best_cmap: 151 return 152 sys.exit('None of characters in %s were found in %s' % (chars, font)) 153 154 155def assert_font_supports_all_of_chars(font, chars): 156 best_cmap = get_best_cmap(font) 157 for char in chars: 158 assert char in best_cmap, ( 159 'U+%04X was not found in %s' % (char, font)) 160 161 162def assert_font_supports_none_of_chars(font, chars): 163 best_cmap = get_best_cmap(font) 164 for char in chars: 165 assert char not in best_cmap, ( 166 'U+%04X was found in %s' % (char, font)) 167 168 169def assert_font_supports_all_sequences(font, sequences): 170 vs_dict = get_variation_sequences_cmap(font).uvsDict 171 for base, vs in sorted(sequences): 172 assert vs in vs_dict and (base, None) in vs_dict[vs], ( 173 '<U+%04X, U+%04X> was not found in %s' % (base, vs, font)) 174 175 176def check_hyphens(hyphens_dir): 177 # Find all the scripts that need automatic hyphenation 178 scripts = set() 179 for hyb_file in glob.iglob(path.join(hyphens_dir, '*.hyb')): 180 hyb_file = path.basename(hyb_file) 181 assert hyb_file.startswith('hyph-'), ( 182 'Unknown hyphenation file %s' % hyb_file) 183 lang_code = hyb_file[hyb_file.index('-')+1:hyb_file.index('.')] 184 scripts.add(lang_to_script(lang_code)) 185 186 HYPHENS = {0x002D, 0x2010} 187 for script in scripts: 188 fonts = _script_to_font_map[script] 189 assert fonts, 'No fonts found for the "%s" script' % script 190 for font in fonts: 191 assert_font_supports_any_of_chars(font, HYPHENS) 192 193 194class FontRecord(object): 195 def __init__(self, name, scripts, variant, weight, style, font): 196 self.name = name 197 self.scripts = scripts 198 self.variant = variant 199 self.weight = weight 200 self.style = style 201 self.font = font 202 203 204def parse_fonts_xml(fonts_xml_path): 205 global _script_to_font_map, _fallback_chain 206 _script_to_font_map = collections.defaultdict(set) 207 _fallback_chain = [] 208 tree = ElementTree.parse(fonts_xml_path) 209 families = tree.findall('family') 210 # Minikin supports up to 254 but users can place their own font at the first 211 # place. Thus, 253 is the maximum allowed number of font families in the 212 # default collection. 213 assert len(families) < 254, ( 214 'System font collection can contains up to 253 font families.') 215 for family in families: 216 name = family.get('name') 217 variant = family.get('variant') 218 langs = family.get('lang') 219 if name: 220 assert variant is None, ( 221 'No variant expected for LGC font %s.' % name) 222 assert langs is None, ( 223 'No language expected for LGC fonts %s.' % name) 224 else: 225 assert variant in {None, 'elegant', 'compact'}, ( 226 'Unexpected value for variant: %s' % variant) 227 228 if langs: 229 langs = langs.split() 230 scripts = {lang_to_script(lang) for lang in langs} 231 else: 232 scripts = set() 233 234 for child in family: 235 assert child.tag == 'font', ( 236 'Unknown tag <%s>' % child.tag) 237 font_file = child.text 238 weight = int(child.get('weight')) 239 assert weight % 100 == 0, ( 240 'Font weight "%d" is not a multiple of 100.' % weight) 241 242 style = child.get('style') 243 assert style in {'normal', 'italic'}, ( 244 'Unknown style "%s"' % style) 245 246 index = child.get('index') 247 if index: 248 index = int(index) 249 250 _fallback_chain.append(FontRecord( 251 name, 252 frozenset(scripts), 253 variant, 254 weight, 255 style, 256 (font_file, index))) 257 258 if name: # non-empty names are used for default LGC fonts 259 map_scripts = {'Latn', 'Grek', 'Cyrl'} 260 else: 261 map_scripts = scripts 262 for script in map_scripts: 263 _script_to_font_map[script].add((font_file, index)) 264 265 266def check_emoji_coverage(all_emoji, equivalent_emoji): 267 emoji_font = get_emoji_font() 268 check_emoji_font_coverage(emoji_font, all_emoji, equivalent_emoji) 269 270 271def get_emoji_font(): 272 emoji_fonts = [ 273 record.font for record in _fallback_chain 274 if 'Zsye' in record.scripts] 275 assert len(emoji_fonts) == 1, 'There are %d emoji fonts.' % len(emoji_fonts) 276 return emoji_fonts[0] 277 278 279def check_emoji_font_coverage(emoji_font, all_emoji, equivalent_emoji): 280 coverage = get_emoji_map(emoji_font) 281 for sequence in all_emoji: 282 assert sequence in coverage, ( 283 '%s is not supported in the emoji font.' % printable(sequence)) 284 285 for sequence in coverage: 286 if sequence in {0x0000, 0x000D, 0x0020}: 287 # The font needs to support a few extra characters, which is OK 288 continue 289 assert sequence in all_emoji, ( 290 'Emoji font should not support %s.' % printable(sequence)) 291 292 for first, second in sorted(equivalent_emoji.items()): 293 assert coverage[first] == coverage[second], ( 294 '%s and %s should map to the same glyph.' % ( 295 printable(first), 296 printable(second))) 297 298 for glyph in set(coverage.values()): 299 maps_to_glyph = [seq for seq in coverage if coverage[seq] == glyph] 300 if len(maps_to_glyph) > 1: 301 # There are more than one sequences mapping to the same glyph. We 302 # need to make sure they were expected to be equivalent. 303 equivalent_seqs = set() 304 for seq in maps_to_glyph: 305 equivalent_seq = seq 306 while equivalent_seq in equivalent_emoji: 307 equivalent_seq = equivalent_emoji[equivalent_seq] 308 equivalent_seqs.add(equivalent_seq) 309 assert len(equivalent_seqs) == 1, ( 310 'The sequences %s should not result in the same glyph %s' % ( 311 printable(equivalent_seqs), 312 glyph)) 313 314 315def check_emoji_defaults(default_emoji): 316 missing_text_chars = _emoji_properties['Emoji'] - default_emoji 317 emoji_font_seen = False 318 for record in _fallback_chain: 319 if 'Zsye' in record.scripts: 320 emoji_font_seen = True 321 # No need to check the emoji font 322 continue 323 # For later fonts, we only check them if they have a script 324 # defined, since the defined script may get them to a higher 325 # score even if they appear after the emoji font. However, 326 # we should skip checking the text symbols font, since 327 # symbol fonts should be able to override the emoji display 328 # style when 'Zsym' is explicitly specified by the user. 329 if emoji_font_seen and (not record.scripts or 'Zsym' in record.scripts): 330 continue 331 332 # Check default emoji-style characters 333 assert_font_supports_none_of_chars(record.font, sorted(default_emoji)) 334 335 # Mark default text-style characters appearing in fonts above the emoji 336 # font as seen 337 if not emoji_font_seen: 338 missing_text_chars -= set(get_best_cmap(record.font)) 339 340 # Noto does not have monochrome glyphs for Unicode 7.0 wingdings and 341 # webdings yet. 342 missing_text_chars -= _chars_by_age['7.0'] 343 assert missing_text_chars == set(), ( 344 'Text style version of some emoji characters are missing: ' + 345 repr(missing_text_chars)) 346 347 348# Setting reverse to true returns a dictionary that maps the values to sets of 349# characters, useful for some binary properties. Otherwise, we get a 350# dictionary that maps characters to the property values, assuming there's only 351# one property in the file. 352def parse_unicode_datafile(file_path, reverse=False): 353 if reverse: 354 output_dict = collections.defaultdict(set) 355 else: 356 output_dict = {} 357 with open(file_path) as datafile: 358 for line in datafile: 359 if '#' in line: 360 line = line[:line.index('#')] 361 line = line.strip() 362 if not line: 363 continue 364 365 chars, prop = line.split(';')[:2] 366 chars = chars.strip() 367 prop = prop.strip() 368 369 if ' ' in chars: # character sequence 370 sequence = [int(ch, 16) for ch in chars.split(' ')] 371 additions = [tuple(sequence)] 372 elif '..' in chars: # character range 373 char_start, char_end = chars.split('..') 374 char_start = int(char_start, 16) 375 char_end = int(char_end, 16) 376 additions = xrange(char_start, char_end+1) 377 else: # singe character 378 additions = [int(chars, 16)] 379 if reverse: 380 output_dict[prop].update(additions) 381 else: 382 for addition in additions: 383 assert addition not in output_dict 384 output_dict[addition] = prop 385 return output_dict 386 387 388def parse_standardized_variants(file_path): 389 emoji_set = set() 390 text_set = set() 391 with open(file_path) as datafile: 392 for line in datafile: 393 if '#' in line: 394 line = line[:line.index('#')] 395 line = line.strip() 396 if not line: 397 continue 398 sequence, description, _ = line.split(';') 399 sequence = sequence.strip().split(' ') 400 base = int(sequence[0], 16) 401 vs = int(sequence[1], 16) 402 description = description.strip() 403 if description == 'text style': 404 text_set.add((base, vs)) 405 elif description == 'emoji style': 406 emoji_set.add((base, vs)) 407 return text_set, emoji_set 408 409 410def parse_ucd(ucd_path): 411 global _emoji_properties, _chars_by_age 412 global _text_variation_sequences, _emoji_variation_sequences 413 global _emoji_sequences, _emoji_zwj_sequences 414 _emoji_properties = parse_unicode_datafile( 415 path.join(ucd_path, 'emoji-data.txt'), reverse=True) 416 _chars_by_age = parse_unicode_datafile( 417 path.join(ucd_path, 'DerivedAge.txt'), reverse=True) 418 sequences = parse_standardized_variants( 419 path.join(ucd_path, 'StandardizedVariants.txt')) 420 _text_variation_sequences, _emoji_variation_sequences = sequences 421 _emoji_sequences = parse_unicode_datafile( 422 path.join(ucd_path, 'emoji-sequences.txt')) 423 _emoji_zwj_sequences = parse_unicode_datafile( 424 path.join(ucd_path, 'emoji-zwj-sequences.txt')) 425 426 427def flag_sequence(territory_code): 428 return tuple(0x1F1E6 + ord(ch) - ord('A') for ch in territory_code) 429 430 431UNSUPPORTED_FLAGS = frozenset({ 432 flag_sequence('BL'), flag_sequence('BQ'), flag_sequence('DG'), 433 flag_sequence('EA'), flag_sequence('EH'), flag_sequence('FK'), 434 flag_sequence('GF'), flag_sequence('GP'), flag_sequence('GS'), 435 flag_sequence('MF'), flag_sequence('MQ'), flag_sequence('NC'), 436 flag_sequence('PM'), flag_sequence('RE'), flag_sequence('TF'), 437 flag_sequence('UN'), flag_sequence('WF'), flag_sequence('XK'), 438 flag_sequence('YT'), 439}) 440 441EQUIVALENT_FLAGS = { 442 flag_sequence('BV'): flag_sequence('NO'), 443 flag_sequence('CP'): flag_sequence('FR'), 444 flag_sequence('HM'): flag_sequence('AU'), 445 flag_sequence('SJ'): flag_sequence('NO'), 446 flag_sequence('UM'): flag_sequence('US'), 447} 448 449COMBINING_KEYCAP = 0x20E3 450 451# Characters that Android defaults to emoji style, different from the recommendations in UTR #51 452ANDROID_DEFAULT_EMOJI = frozenset({ 453 0x2600, # BLACK SUN WITH RAYS 454 0x2601, # CLOUD 455 0x260E, # BLACK TELEPHONE 456 0x261D, # WHITE UP POINTING INDEX 457 0x263A, # WHITE SMILING FACE 458 0x2660, # BLACK SPADE SUIT 459 0x2663, # BLACK CLUB SUIT 460 0x2665, # BLACK HEART SUIT 461 0x2666, # BLACK DIAMOND SUIT 462 0x270C, # VICTORY HAND 463 0x2744, # SNOWFLAKE 464 0x2764, # HEAVY BLACK HEART 465}) 466 467LEGACY_ANDROID_EMOJI = { 468 0xFE4E5: flag_sequence('JP'), 469 0xFE4E6: flag_sequence('US'), 470 0xFE4E7: flag_sequence('FR'), 471 0xFE4E8: flag_sequence('DE'), 472 0xFE4E9: flag_sequence('IT'), 473 0xFE4EA: flag_sequence('GB'), 474 0xFE4EB: flag_sequence('ES'), 475 0xFE4EC: flag_sequence('RU'), 476 0xFE4ED: flag_sequence('CN'), 477 0xFE4EE: flag_sequence('KR'), 478 0xFE82C: (ord('#'), COMBINING_KEYCAP), 479 0xFE82E: (ord('1'), COMBINING_KEYCAP), 480 0xFE82F: (ord('2'), COMBINING_KEYCAP), 481 0xFE830: (ord('3'), COMBINING_KEYCAP), 482 0xFE831: (ord('4'), COMBINING_KEYCAP), 483 0xFE832: (ord('5'), COMBINING_KEYCAP), 484 0xFE833: (ord('6'), COMBINING_KEYCAP), 485 0xFE834: (ord('7'), COMBINING_KEYCAP), 486 0xFE835: (ord('8'), COMBINING_KEYCAP), 487 0xFE836: (ord('9'), COMBINING_KEYCAP), 488 0xFE837: (ord('0'), COMBINING_KEYCAP), 489} 490 491ZWJ_IDENTICALS = { 492 # KISS 493 (0x1F469, 0x200D, 0x2764, 0x200D, 0x1F48B, 0x200D, 0x1F468): 0x1F48F, 494 # COUPLE WITH HEART 495 (0x1F469, 0x200D, 0x2764, 0x200D, 0x1F468): 0x1F491, 496 # FAMILY 497 (0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F466): 0x1F46A, 498} 499 500 501def is_fitzpatrick_modifier(cp): 502 return 0x1F3FB <= cp <= 0x1F3FF 503 504 505def reverse_emoji(seq): 506 rev = list(reversed(seq)) 507 # if there are fitzpatrick modifiers in the sequence, keep them after 508 # the emoji they modify 509 for i in xrange(1, len(rev)): 510 if is_fitzpatrick_modifier(rev[i-1]): 511 rev[i], rev[i-1] = rev[i-1], rev[i] 512 return tuple(rev) 513 514 515def compute_expected_emoji(): 516 equivalent_emoji = {} 517 sequence_pieces = set() 518 all_sequences = set() 519 all_sequences.update(_emoji_variation_sequences) 520 521 # add zwj sequences not in the current emoji-zwj-sequences.txt 522 adjusted_emoji_zwj_sequences = dict(_emoji_zwj_sequences) 523 adjusted_emoji_zwj_sequences.update(_emoji_zwj_sequences) 524 # single parent families 525 additional_emoji_zwj = ( 526 (0x1F468, 0x200D, 0x1F466), 527 (0x1F468, 0x200D, 0x1F467), 528 (0x1F468, 0x200D, 0x1F466, 0x200D, 0x1F466), 529 (0x1F468, 0x200D, 0x1F467, 0x200D, 0x1F466), 530 (0x1F468, 0x200D, 0x1F467, 0x200D, 0x1F467), 531 (0x1F469, 0x200D, 0x1F466), 532 (0x1F469, 0x200D, 0x1F467), 533 (0x1F469, 0x200D, 0x1F466, 0x200D, 0x1F466), 534 (0x1F469, 0x200D, 0x1F467, 0x200D, 0x1F466), 535 (0x1F469, 0x200D, 0x1F467, 0x200D, 0x1F467), 536 ) 537 # sequences formed from man and woman and optional fitzpatrick modifier 538 modified_extensions = ( 539 0x2696, 540 0x2708, 541 0x1F3A8, 542 0x1F680, 543 0x1F692, 544 ) 545 for seq in additional_emoji_zwj: 546 adjusted_emoji_zwj_sequences[seq] = 'Emoji_ZWJ_Sequence' 547 for ext in modified_extensions: 548 for base in (0x1F468, 0x1F469): 549 seq = (base, 0x200D, ext) 550 adjusted_emoji_zwj_sequences[seq] = 'Emoji_ZWJ_Sequence' 551 for modifier in range(0x1F3FB, 0x1F400): 552 seq = (base, modifier, 0x200D, ext) 553 adjusted_emoji_zwj_sequences[seq] = 'Emoji_ZWJ_Sequence' 554 555 for sequence in _emoji_sequences.keys(): 556 sequence = tuple(ch for ch in sequence if ch != EMOJI_VS) 557 all_sequences.add(sequence) 558 sequence_pieces.update(sequence) 559 560 for sequence in adjusted_emoji_zwj_sequences.keys(): 561 sequence = tuple(ch for ch in sequence if ch != EMOJI_VS) 562 all_sequences.add(sequence) 563 sequence_pieces.update(sequence) 564 # Add reverse of all emoji ZWJ sequences, which are added to the fonts 565 # as a workaround to get the sequences work in RTL text. 566 reversed_seq = reverse_emoji(sequence) 567 all_sequences.add(reversed_seq) 568 equivalent_emoji[reversed_seq] = sequence 569 570 # Add all two-letter flag sequences, as even the unsupported ones should 571 # resolve to a flag tofu. 572 all_letters = [chr(code) for code in range(ord('A'), ord('Z')+1)] 573 all_two_letter_codes = itertools.product(all_letters, repeat=2) 574 all_flags = {flag_sequence(code) for code in all_two_letter_codes} 575 all_sequences.update(all_flags) 576 tofu_flags = UNSUPPORTED_FLAGS | (all_flags - set(_emoji_sequences.keys())) 577 578 all_emoji = ( 579 _emoji_properties['Emoji'] | 580 all_sequences | 581 sequence_pieces | 582 set(LEGACY_ANDROID_EMOJI.keys())) 583 default_emoji = ( 584 _emoji_properties['Emoji_Presentation'] | 585 ANDROID_DEFAULT_EMOJI | 586 all_sequences | 587 set(LEGACY_ANDROID_EMOJI.keys())) 588 589 first_tofu_flag = sorted(tofu_flags)[0] 590 for flag in tofu_flags: 591 if flag != first_tofu_flag: 592 equivalent_emoji[flag] = first_tofu_flag 593 equivalent_emoji.update(EQUIVALENT_FLAGS) 594 equivalent_emoji.update(LEGACY_ANDROID_EMOJI) 595 equivalent_emoji.update(ZWJ_IDENTICALS) 596 for seq in _emoji_variation_sequences: 597 equivalent_emoji[seq] = seq[0] 598 599 return all_emoji, default_emoji, equivalent_emoji 600 601 602def check_vertical_metrics(): 603 for record in _fallback_chain: 604 if record.name in ['sans-serif', 'sans-serif-condensed']: 605 font = open_font(record.font) 606 assert font['head'].yMax == 2163 and font['head'].yMin == -555, ( 607 'yMax and yMin of %s do not match expected values.' % (record.font,)) 608 609 if record.name in ['sans-serif', 'sans-serif-condensed', 'serif', 'monospace']: 610 font = open_font(record.font) 611 assert font['hhea'].ascent == 1900 and font['hhea'].descent == -500, ( 612 'ascent and descent of %s do not match expected values.' % (record.font,)) 613 614 615def main(): 616 global _fonts_dir 617 target_out = sys.argv[1] 618 _fonts_dir = path.join(target_out, 'fonts') 619 620 fonts_xml_path = path.join(target_out, 'etc', 'fonts.xml') 621 parse_fonts_xml(fonts_xml_path) 622 623 check_vertical_metrics() 624 625 hyphens_dir = path.join(target_out, 'usr', 'hyphen-data') 626 check_hyphens(hyphens_dir) 627 628 check_emoji = sys.argv[2] 629 if check_emoji == 'true': 630 ucd_path = sys.argv[3] 631 parse_ucd(ucd_path) 632 all_emoji, default_emoji, equivalent_emoji = compute_expected_emoji() 633 check_emoji_coverage(all_emoji, equivalent_emoji) 634 check_emoji_defaults(default_emoji) 635 636 637if __name__ == '__main__': 638 main() 639