Home | History | Annotate | Download | only in emoji-compat
      1 #!/usr/bin/python
      2 #
      3 # Copyright (C) 2017 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """
     18 Creates the EmojiCompat font with the metadata. Metadata is embedded in FlatBuffers binary format
     19 under a meta tag with name 'Emji'.
     20 
     21 In order to create the final font the followings are used as inputs:
     22 
     23 - NotoColorEmoji.ttf: Emoji font in the Android framework. Currently at
     24 external/noto-fonts/emoji/NotoColorEmoji.ttf
     25 
     26 - Unicode files: Unicode files that are in the framework, and lists information about all the
     27 emojis. These files are emoji-data.txt, emoji-sequences.txt, emoji-zwj-sequences.txt,
     28 and emoji-variation-sequences.txt. Currently at external/unicode/.
     29 
     30 - additions/emoji-zwj-sequences.txt: Includes emojis that are not defined in Unicode files, but are
     31 in the Android font. Resides in framework and currently under external/unicode/.
     32 
     33 - ../third_party/unicode/emoji_metadata.txt: The file that includes the id, codepoints, the first
     34 Android OS version that the emoji was added (sdkAdded), and finally the first EmojiCompat font
     35 version that the emoji was added (compatAdded). Updated when the script is executed.
     36 
     37 - data/emoji_metadata.fbs: The flatbuffer schema file. See http://google.github.io/flatbuffers/.
     38 
     39 After execution the following files are generated if they don't exist otherwise, they are updated:
     40 - font/NotoColorEmojiCompat.ttf
     41 - supported-emojis/emojis.txt
     42 - data/emoji_metadata.txt
     43 - src/java/android/support/text/emoji/flatbuffer/*
     44 """
     45 
     46 from __future__ import print_function
     47 
     48 import contextlib
     49 import csv
     50 import json
     51 import os
     52 import shutil
     53 import sys
     54 import tempfile
     55 from fontTools import ttLib
     56 
     57 
     58 ########### UPDATE OR CHECK WHEN A NEW FONT IS BEING GENERATED ###########
     59 # Last Android SDK Version
     60 SDK_VERSION = 26
     61 # metadata version that will be embedded into font.
     62 METADATA_VERSION = 2
     63 
     64 ####### main directories where output files are created #######
     65 SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
     66 FONT_DIR = os.path.join(SCRIPT_DIR, 'font')
     67 DATA_DIR = os.path.join(SCRIPT_DIR, 'data')
     68 SUPPORTED_EMOJIS_DIR = os.path.join(SCRIPT_DIR, 'supported-emojis')
     69 JAVA_SRC_DIR = os.path.join(SCRIPT_DIR, 'src', 'java')
     70 ####### output files #######
     71 # font file
     72 FONT_PATH = os.path.join(FONT_DIR, 'NotoColorEmojiCompat.ttf')
     73 # emoji metadata json output file
     74 OUTPUT_META_FILE = os.path.join(DATA_DIR, 'emoji_metadata.txt')
     75 # emojis test file
     76 TEST_DATA_PATH = os.path.join(SUPPORTED_EMOJIS_DIR, 'emojis.txt')
     77 ####### input files #######
     78 # Unicode file names to read emoji data
     79 EMOJI_DATA_FILE = 'emoji-data.txt'
     80 EMOJI_SEQ_FILE = 'emoji-sequences.txt'
     81 EMOJI_ZWJ_FILE = 'emoji-zwj-sequences.txt'
     82 EMOJI_VARIATION_SEQ_FILE = 'emoji-variation-sequences.txt'
     83 # Android OS emoji file for emojis that are not in Unicode files
     84 ANDROID_EMOJI_ZWJ_SEQ_FILE = os.path.join('additions', 'emoji-zwj-sequences.txt')
     85 ANDROID_EMOJIS_SEQ_FILE = os.path.join('additions', 'emoji-sequences.txt')
     86 # Android OS emoji style override file. Codepoints that are rendered with emoji style by default
     87 # even though not defined so in <code>emoji-data.txt</code>.
     88 EMOJI_STYLE_OVERRIDE_FILE = os.path.join('additions', 'emoji-data.txt')
     89 # emoji metadata file
     90 INPUT_META_FILE = OUTPUT_META_FILE
     91 # flatbuffer schema
     92 FLATBUFFER_SCHEMA = os.path.join(DATA_DIR, 'emoji_metadata.fbs')
     93 # file path for java header, it will be prepended to flatbuffer java files
     94 FLATBUFFER_HEADER = os.path.join(DATA_DIR, "flatbuffer_header.txt")
     95 # temporary emoji metadata json output file
     96 OUTPUT_JSON_FILE_NAME = 'emoji_metadata.json'
     97 # temporary binary file generated by flatbuffer
     98 FLATBUFFER_BIN = 'emoji_metadata.bin'
     99 # directory representation for flatbuffer java package
    100 FLATBUFFER_PACKAGE_PATH = os.path.join('android', 'support', 'text', 'emoji', 'flatbuffer', '')
    101 # temporary directory that contains flatbuffer java files
    102 FLATBUFFER_JAVA_PATH = os.path.join(FLATBUFFER_PACKAGE_PATH)
    103 FLATBUFFER_METADATA_LIST_JAVA = "MetadataList.java"
    104 FLATBUFFER_METADATA_ITEM_JAVA = "MetadataItem.java"
    105 # directory under source where flatbuffer java files will be copied into
    106 FLATBUFFER_JAVA_TARGET = os.path.join(JAVA_SRC_DIR, FLATBUFFER_PACKAGE_PATH)
    107 # meta tag name used in the font to embed the emoji metadata. This value is also used in
    108 # MetadataListReader.java in order to locate the metadata location.
    109 EMOJI_META_TAG_NAME = 'Emji'
    110 
    111 EMOJI_PRESENTATION_STR = 'EMOJI_PRESENTATION'
    112 STD_VARIANTS_EMOJI_STYLE = 'EMOJI STYLE'
    113 
    114 DEFAULT_EMOJI_ID = 0xF0001
    115 EMOJI_STYLE_VS = 0xFE0F
    116 
    117 def to_hex_str(value):
    118     """Converts given int value to hex without the 0x prefix"""
    119     return format(value, 'X')
    120 
    121 def hex_str_to_int(string):
    122     """Convert a hex string into int"""
    123     return int(string, 16)
    124 
    125 def codepoint_to_string(codepoints):
    126     """Converts a list of codepoints into a string separated with space."""
    127     return ' '.join([to_hex_str(x) for x in codepoints])
    128 
    129 def prepend_header_to_file(file_path):
    130     """Prepends the header to the file. Used to update flatbuffer java files with header, comments
    131     and annotations."""
    132     with open(file_path, "r+") as original_file:
    133         with open(FLATBUFFER_HEADER, "r") as copyright_file:
    134             original_content = original_file.read()
    135             start_index = original_content.index("public final class")
    136             original_file.seek(0)
    137             original_file.write(copyright_file.read() + "\n" + original_content[start_index:])
    138 
    139 
    140 def update_flatbuffer_java_files(flatbuffer_java_dir):
    141     """Prepends headers to flatbuffer java files and copies to the final destination"""
    142     tmp_metadata_list = flatbuffer_java_dir + FLATBUFFER_METADATA_LIST_JAVA
    143     tmp_metadata_item = flatbuffer_java_dir + FLATBUFFER_METADATA_ITEM_JAVA
    144     prepend_header_to_file(tmp_metadata_list)
    145     prepend_header_to_file(tmp_metadata_item)
    146 
    147     if not os.path.exists(FLATBUFFER_JAVA_TARGET):
    148         os.makedirs(FLATBUFFER_JAVA_TARGET)
    149 
    150     shutil.copy(tmp_metadata_list, FLATBUFFER_JAVA_TARGET + FLATBUFFER_METADATA_LIST_JAVA)
    151     shutil.copy(tmp_metadata_item, FLATBUFFER_JAVA_TARGET + FLATBUFFER_METADATA_ITEM_JAVA)
    152 
    153 def create_test_data(unicode_path):
    154     """Read all the emojis in the unicode files and update the test file"""
    155     lines = read_emoji_lines(os.path.join(unicode_path, EMOJI_ZWJ_FILE))
    156     lines += read_emoji_lines(os.path.join(unicode_path, EMOJI_SEQ_FILE))
    157 
    158     lines += read_emoji_lines(os.path.join(unicode_path, ANDROID_EMOJI_ZWJ_SEQ_FILE), optional=True)
    159     lines += read_emoji_lines(os.path.join(unicode_path, ANDROID_EMOJIS_SEQ_FILE), optional=True)
    160 
    161     # standardized variants contains a huge list of sequences, only read the ones that are emojis
    162     # and also the ones with FE0F (emoji style)
    163     standardized_variants_lines = read_emoji_lines(
    164         os.path.join(unicode_path, EMOJI_VARIATION_SEQ_FILE))
    165     for line in standardized_variants_lines:
    166         if STD_VARIANTS_EMOJI_STYLE in line:
    167             lines.append(line)
    168 
    169     emojis_set = set()
    170     for line in lines:
    171         codepoints = [hex_str_to_int(x) for x in line.split(';')[0].strip().split(' ')]
    172         emojis_set.add(codepoint_to_string(codepoints).upper())
    173 
    174     emoji_data_lines = read_emoji_lines(os.path.join(unicode_path, EMOJI_DATA_FILE))
    175     for line in emoji_data_lines:
    176         codepoints_range, emoji_property = codepoints_and_emoji_prop(line)
    177         is_emoji_style = emoji_property == EMOJI_PRESENTATION_STR
    178         if is_emoji_style:
    179             codepoints = [to_hex_str(x) for x in
    180                           codepoints_for_emojirange(codepoints_range)]
    181             emojis_set.update(codepoints)
    182 
    183     emoji_style_exceptions = get_emoji_style_exceptions(unicode_path)
    184     #  finally add the android default emoji exceptions
    185     emojis_set.update([to_hex_str(x) for x in emoji_style_exceptions])
    186 
    187     emojis_list = list(emojis_set)
    188     emojis_list.sort()
    189     with open(TEST_DATA_PATH, "w") as test_file:
    190         for line in emojis_list:
    191             test_file.write("%s\n" % line)
    192 
    193 class _EmojiData(object):
    194     """Holds the information about a single emoji."""
    195 
    196     def __init__(self, codepoints, is_emoji_style):
    197         self.codepoints = codepoints
    198         self.emoji_style = is_emoji_style
    199         self.emoji_id = 0
    200         self.width = 0
    201         self.height = 0
    202         self.sdk_added = SDK_VERSION
    203         self.compat_added = METADATA_VERSION
    204 
    205     def update_metrics(self, metrics):
    206         """Updates width/height instance variables with the values given in metrics dictionary.
    207         :param metrics: a dictionary object that has width and height values.
    208         """
    209         self.width = metrics.width
    210         self.height = metrics.height
    211 
    212     def __repr__(self):
    213         return '<EmojiData {0} - {1}>'.format(self.emoji_style,
    214                                               codepoint_to_string(self.codepoints))
    215 
    216     def create_json_element(self):
    217         """Creates the json representation of EmojiData."""
    218         json_element = {}
    219         json_element['id'] = self.emoji_id
    220         json_element['emojiStyle'] = self.emoji_style
    221         json_element['sdkAdded'] = self.sdk_added
    222         json_element['compatAdded'] = self.compat_added
    223         json_element['width'] = self.width
    224         json_element['height'] = self.height
    225         json_element['codepoints'] = self.codepoints
    226         return json_element
    227 
    228     def create_txt_row(self):
    229         """Creates array of values for CSV of EmojiData."""
    230         row = [to_hex_str(self.emoji_id), self.sdk_added, self.compat_added]
    231         row += [to_hex_str(x) for x in self.codepoints]
    232         return row
    233 
    234     def update(self, emoji_id, sdk_added, compat_added):
    235         """Updates current EmojiData with the values in a json element"""
    236         self.emoji_id = emoji_id
    237         self.sdk_added = sdk_added
    238         self.compat_added = compat_added
    239 
    240 
    241 def read_emoji_lines(file_path, optional=False):
    242     """Read all lines in an unicode emoji file into a list of uppercase strings. Ignore the empty
    243     lines and comments
    244     :param file_path: unicode emoji file path
    245     :param optional: if True no exception is raised when the file cannot be read
    246     :return: list of uppercase strings
    247     """
    248     result = []
    249     try:
    250         with open(file_path) as file_stream:
    251             for line in file_stream:
    252                 line = line.strip()
    253                 if line and not line.startswith('#'):
    254                     result.append(line.upper())
    255     except IOError:
    256         if optional:
    257             pass
    258         else:
    259             raise
    260 
    261     return result
    262 
    263 def get_emoji_style_exceptions(unicode_path):
    264     """Read EMOJI_STYLE_OVERRIDE_FILE and return the codepoints as integers"""
    265     lines = read_emoji_lines(os.path.join(unicode_path, EMOJI_STYLE_OVERRIDE_FILE))
    266     exceptions = []
    267     for line in lines:
    268         codepoint = hex_str_to_int(codepoints_and_emoji_prop(line)[0])
    269         exceptions.append(codepoint)
    270     return exceptions
    271 
    272 def codepoints_for_emojirange(codepoints_range):
    273     """ Return codepoints given in emoji files. Expand the codepoints that are given as a range
    274     such as XYZ ... UVT
    275     """
    276     codepoints = []
    277     if '..' in codepoints_range:
    278         range_start, range_end = codepoints_range.split('..')
    279         codepoints_range = range(hex_str_to_int(range_start),
    280                                  hex_str_to_int(range_end) + 1)
    281         codepoints.extend(codepoints_range)
    282     else:
    283         codepoints.append(hex_str_to_int(codepoints_range))
    284     return codepoints
    285 
    286 def codepoints_and_emoji_prop(line):
    287     """For a given emoji file line, return codepoints and emoji property in the line.
    288     1F93C..1F93E ; [Emoji|Emoji_Presentation|Emoji_Modifier_Base] # [...]"""
    289     line = line.strip()
    290     if '#' in line:
    291         line = line[:line.index('#')]
    292     else:
    293         raise ValueError("Line is expected to have # in it")
    294     line = line.split(';')
    295     codepoints_range = line[0].strip()
    296     emoji_property = line[1].strip()
    297 
    298     return codepoints_range, emoji_property
    299 
    300 def read_emoji_intervals(emoji_data_map, file_path, emoji_style_exceptions):
    301     """Read unicode lines of unicode emoji file in which each line describes a set of codepoint
    302     intervals. Expands the interval on a line and inserts related EmojiDatas into emoji_data_map.
    303     A line format that is expected is as follows:
    304     1F93C..1F93E ; [Emoji|Emoji_Presentation|Emoji_Modifier_Base] # [...]"""
    305     lines = read_emoji_lines(file_path)
    306 
    307     for line in lines:
    308         codepoints_range, emoji_property = codepoints_and_emoji_prop(line)
    309         is_emoji_style = emoji_property == EMOJI_PRESENTATION_STR
    310         codepoints = codepoints_for_emojirange(codepoints_range)
    311 
    312         for codepoint in codepoints:
    313             key = codepoint_to_string([codepoint])
    314             codepoint_is_emoji_style = is_emoji_style or codepoint in emoji_style_exceptions
    315             if key in emoji_data_map:
    316                 # since there are multiple definitions of emojis, only update when emoji style is
    317                 # True
    318                 if codepoint_is_emoji_style:
    319                     emoji_data_map[key].emoji_style = True
    320             else:
    321                 emoji_data = _EmojiData([codepoint], codepoint_is_emoji_style)
    322                 emoji_data_map[key] = emoji_data
    323 
    324 
    325 def read_emoji_sequences(emoji_data_map, file_path, optional=False):
    326     """Reads the content of the file which contains emoji sequences. Creates EmojiData for each
    327     line and puts into emoji_data_map."""
    328     lines = read_emoji_lines(file_path, optional)
    329     # 1F1E6 1F1E8 ; Name ; [...]
    330     for line in lines:
    331         codepoints = [hex_str_to_int(x) for x in line.split(';')[0].strip().split(' ')]
    332         codepoints = [x for x in codepoints if x != EMOJI_STYLE_VS]
    333         key = codepoint_to_string(codepoints)
    334         if not key in emoji_data_map:
    335             emoji_data = _EmojiData(codepoints, False)
    336             emoji_data_map[key] = emoji_data
    337 
    338 
    339 def load_emoji_data_map(unicode_path):
    340     """Reads the emoji data files, constructs a map of space separated codepoints to EmojiData.
    341     :return: map of space separated codepoints to EmojiData
    342     """
    343     emoji_data_map = {}
    344     emoji_style_exceptions = get_emoji_style_exceptions(unicode_path)
    345     read_emoji_intervals(emoji_data_map, os.path.join(unicode_path, EMOJI_DATA_FILE),
    346                          emoji_style_exceptions)
    347     read_emoji_sequences(emoji_data_map, os.path.join(unicode_path, EMOJI_ZWJ_FILE))
    348     read_emoji_sequences(emoji_data_map, os.path.join(unicode_path, EMOJI_SEQ_FILE))
    349 
    350     # Add the optional ANDROID_EMOJI_ZWJ_SEQ_FILE if it exists.
    351     read_emoji_sequences(emoji_data_map, os.path.join(unicode_path, ANDROID_EMOJI_ZWJ_SEQ_FILE),
    352                          optional=True)
    353     # Add the optional ANDROID_EMOJIS_SEQ_FILE if it exists.
    354     read_emoji_sequences(emoji_data_map, os.path.join(unicode_path, ANDROID_EMOJIS_SEQ_FILE),
    355                          optional=True)
    356 
    357     return emoji_data_map
    358 
    359 
    360 def load_previous_metadata(emoji_data_map):
    361     """Updates emoji data elements in emoji_data_map using the id, sdk_added and compat_added fields
    362        in emoji_metadata.txt. Returns the smallest available emoji id to use. i.e. if the largest
    363        emoji id emoji_metadata.txt is 1, function would return 2. If emoji_metadata.txt does not
    364        exist, or contains no emojis defined returns DEFAULT_EMOJI_ID"""
    365     current_emoji_id = DEFAULT_EMOJI_ID
    366     if os.path.isfile(INPUT_META_FILE):
    367         with open(INPUT_META_FILE) as csvfile:
    368             reader = csv.reader(csvfile, delimiter=' ')
    369             for row in reader:
    370                 if row[0].startswith('#'):
    371                     continue
    372                 emoji_id = hex_str_to_int(row[0])
    373                 sdk_added = int(row[1])
    374                 compat_added = int(row[2])
    375                 key = codepoint_to_string(hex_str_to_int(x) for x in row[3:])
    376                 if key in emoji_data_map:
    377                     emoji_data = emoji_data_map[key]
    378                     emoji_data.update(emoji_id, sdk_added, compat_added)
    379                     if emoji_data.emoji_id >= current_emoji_id:
    380                         current_emoji_id = emoji_data.emoji_id + 1
    381 
    382     return current_emoji_id
    383 
    384 
    385 def update_ttlib_orig_sort():
    386     """Updates the ttLib tag sort with a closure that makes the meta table first."""
    387     orig_sort = ttLib.sortedTagList
    388 
    389     def meta_first_table_sort(tag_list, table_order=None):
    390         """Sorts the tables with the original ttLib sort, then makes the meta table first."""
    391         tag_list = orig_sort(tag_list, table_order)
    392         tag_list.remove('meta')
    393         tag_list.insert(0, 'meta')
    394         return tag_list
    395 
    396     ttLib.sortedTagList = meta_first_table_sort
    397 
    398 
    399 def inject_meta_into_font(ttf, flatbuffer_bin_filename):
    400     """inject metadata binary into font"""
    401     if not 'meta' in ttf:
    402         ttf['meta'] = ttLib.getTableClass('meta')()
    403     meta = ttf['meta']
    404     with contextlib.closing(open(flatbuffer_bin_filename)) as flatbuffer_bin_file:
    405         meta.data[EMOJI_META_TAG_NAME] = flatbuffer_bin_file.read()
    406 
    407     # sort meta tables for faster access
    408     update_ttlib_orig_sort()
    409 
    410 
    411 def validate_input_files(font_path, unicode_path):
    412     """Validate the existence of font file and the unicode files"""
    413     if not os.path.isfile(font_path):
    414         raise ValueError("Font file does not exist: " + font_path)
    415 
    416     if not os.path.isdir(unicode_path):
    417         raise ValueError(
    418             "Unicode directory does not exist or is not a directory " + unicode_path)
    419 
    420     emoji_filenames = [os.path.join(unicode_path, EMOJI_DATA_FILE),
    421                        os.path.join(unicode_path, EMOJI_ZWJ_FILE),
    422                        os.path.join(unicode_path, EMOJI_SEQ_FILE)]
    423     for emoji_filename in emoji_filenames:
    424         if not os.path.isfile(emoji_filename):
    425             raise ValueError("Unicode emoji data file does not exist: " + emoji_filename)
    426 
    427 
    428 class EmojiFontCreator(object):
    429     """Creates the EmojiCompat font"""
    430 
    431     def __init__(self, font_path, unicode_path):
    432         validate_input_files(font_path, unicode_path)
    433 
    434         self.font_path = font_path
    435         self.unicode_path = unicode_path
    436         self.emoji_data_map = {}
    437         self.remapped_codepoints = {}
    438         self.glyph_to_image_metrics_map = {}
    439         # set default emoji id to start of Supplemental Private Use Area-A
    440         self.emoji_id = DEFAULT_EMOJI_ID
    441 
    442     def update_emoji_data(self, codepoints, glyph_name):
    443         """Updates the existing EmojiData identified with codepoints. The fields that are set are:
    444         - emoji_id (if it does not exist)
    445         - image width/height"""
    446         key = codepoint_to_string(codepoints)
    447         if key in self.emoji_data_map:
    448             # add emoji to final data
    449             emoji_data = self.emoji_data_map[key]
    450             emoji_data.update_metrics(self.glyph_to_image_metrics_map[glyph_name])
    451             if emoji_data.emoji_id == 0:
    452                 emoji_data.emoji_id = self.emoji_id
    453                 self.emoji_id = self.emoji_id + 1
    454             self.remapped_codepoints[emoji_data.emoji_id] = glyph_name
    455 
    456     def read_cbdt(self, ttf):
    457         """Read image size data from CBDT."""
    458         cbdt = ttf['CBDT']
    459         for strike_data in cbdt.strikeData:
    460             for key, data in strike_data.iteritems():
    461                 data.decompile()
    462                 self.glyph_to_image_metrics_map[key] = data.metrics
    463 
    464     def read_cmap12(self, ttf, glyph_to_codepoint_map):
    465         """Reads single code point emojis that are in cmap12, updates glyph_to_codepoint_map and
    466         finally clears all elements in CMAP 12"""
    467         cmap = ttf['cmap']
    468         for table in cmap.tables:
    469             if table.format == 12 and table.platformID == 3 and table.platEncID == 10:
    470                 for codepoint, glyph_name in table.cmap.iteritems():
    471                     glyph_to_codepoint_map[glyph_name] = codepoint
    472                     self.update_emoji_data([codepoint], glyph_name)
    473                 return table
    474         raise ValueError("Font doesn't contain cmap with format:12, platformID:3 and platEncID:10")
    475 
    476     def read_gsub(self, ttf, glyph_to_codepoint_map):
    477         """Reads the emoji sequences defined in GSUB and clear all elements under GSUB"""
    478         gsub = ttf['GSUB']
    479         for lookup in gsub.table.LookupList.Lookup:
    480             for subtable in lookup.SubTable:
    481                 if hasattr(subtable, 'ligatures'):
    482                     for name, ligatures in subtable.ligatures.iteritems():
    483                         for ligature in ligatures:
    484                             glyph_names = [name] + ligature.Component
    485                             codepoints = [glyph_to_codepoint_map[x] for x in glyph_names]
    486                             self.update_emoji_data(codepoints, ligature.LigGlyph)
    487 
    488     def write_metadata_json(self, output_json_file_path):
    489         """Writes the emojis into a json file"""
    490         output_json = {}
    491         output_json['version'] = METADATA_VERSION
    492         output_json['list'] = []
    493 
    494         emoji_data_list = sorted(self.emoji_data_map.values(), key=lambda x: x.emoji_id)
    495 
    496         total_emoji_count = 0
    497         for emoji_data in emoji_data_list:
    498             element = emoji_data.create_json_element()
    499             output_json['list'].append(element)
    500             total_emoji_count = total_emoji_count + 1
    501 
    502         # write the new json file to be processed by FlatBuffers
    503         with open(output_json_file_path, 'w') as json_file:
    504             print(json.dumps(output_json, indent=4, sort_keys=True, separators=(',', ':')),
    505                   file=json_file)
    506 
    507         return total_emoji_count
    508 
    509     def write_metadata_csv(self):
    510         """Writes emoji metadata into space separated file"""
    511         with open(OUTPUT_META_FILE, 'w') as csvfile:
    512             csvwriter = csv.writer(csvfile, delimiter=' ')
    513             emoji_data_list = sorted(self.emoji_data_map.values(), key=lambda x: x.emoji_id)
    514             csvwriter.writerow(['#id', 'sdkAdded', 'compatAdded', 'codepoints'])
    515             for emoji_data in emoji_data_list:
    516                 csvwriter.writerow(emoji_data.create_txt_row())
    517 
    518     def create_font(self):
    519         """Creates the EmojiCompat font.
    520         :param font_path: path to Android NotoColorEmoji font
    521         :param unicode_path: path to directory that contains unicode files
    522         """
    523 
    524         tmp_dir = tempfile.mkdtemp()
    525 
    526         # create emoji codepoints to EmojiData map
    527         self.emoji_data_map = load_emoji_data_map(self.unicode_path)
    528 
    529         # read previous metadata file to update id, sdkAdded and compatAdded. emoji id that is
    530         # returned is either default or 1 greater than the largest id in previous data
    531         self.emoji_id = load_previous_metadata(self.emoji_data_map)
    532 
    533         # recalcTimestamp parameter will keep the modified field same as the original font. Changing
    534         # the modified field in the font causes the font ttf file to change, which makes it harder
    535         # to understand if something really changed in the font.
    536         with contextlib.closing(ttLib.TTFont(self.font_path, recalcTimestamp=False)) as ttf:
    537             # read image size data
    538             self.read_cbdt(ttf)
    539 
    540             # glyph name to codepoint map
    541             glyph_to_codepoint_map = {}
    542 
    543             # read single codepoint emojis under cmap12 and clear the table contents
    544             cmap12_table = self.read_cmap12(ttf, glyph_to_codepoint_map)
    545 
    546             # read emoji sequences gsub and clear the table contents
    547             self.read_gsub(ttf, glyph_to_codepoint_map)
    548 
    549             # add all new codepoint to glyph mappings
    550             cmap12_table.cmap.update(self.remapped_codepoints)
    551 
    552             output_json_file = os.path.join(tmp_dir, OUTPUT_JSON_FILE_NAME)
    553             flatbuffer_bin_file = os.path.join(tmp_dir, FLATBUFFER_BIN)
    554             flatbuffer_java_dir = os.path.join(tmp_dir, FLATBUFFER_JAVA_PATH)
    555 
    556             total_emoji_count = self.write_metadata_json(output_json_file)
    557             self.write_metadata_csv()
    558 
    559             # create the flatbuffers binary and java classes
    560             sys_command = 'flatc -o {0} -b -j {1} {2}'
    561             os.system(sys_command.format(tmp_dir, FLATBUFFER_SCHEMA, output_json_file))
    562 
    563             # inject metadata binary into font
    564             inject_meta_into_font(ttf, flatbuffer_bin_file)
    565 
    566             # save the new font
    567             ttf.save(FONT_PATH)
    568 
    569             update_flatbuffer_java_files(flatbuffer_java_dir)
    570 
    571             create_test_data(self.unicode_path)
    572 
    573             # clear the tmp output directory
    574             shutil.rmtree(tmp_dir, ignore_errors=True)
    575 
    576             print(
    577                 "{0} emojis are written to\n{1}".format(total_emoji_count, FONT_DIR))
    578 
    579 
    580 def print_usage():
    581     """Prints how to use the script."""
    582     print("Please specify a path to font and unicode files.\n"
    583           "usage: createfont.py noto-color-emoji-path unicode-dir-path")
    584 
    585 
    586 if __name__ == '__main__':
    587     if len(sys.argv) < 3:
    588         print_usage()
    589         sys.exit(1)
    590     EmojiFontCreator(sys.argv[1], sys.argv[2]).create_font()
    591