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