zulip/tools/setup/emoji/build_emoji

296 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
#
# See docs/subsystems/emoji.md for a high-level explanation of how this system
# works.
import os
import shutil
import sys
import ujson
from typing import Any, Dict, List, Optional
from emoji_setup_utils import generate_emoji_catalog, generate_codepoint_to_name_map, \
get_emoji_code, generate_name_to_codepoint_map, emoji_names_for_picker, \
EMOTICON_CONVERSIONS, REMAPPED_EMOJIS
from emoji_names import EMOJI_NAME_MAPS
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../')
sys.path.append(ZULIP_PATH)
from scripts.lib.zulip_tools import generate_sha1sum_emoji
TARGET_EMOJI_DUMP = os.path.join(ZULIP_PATH, 'static', 'generated', 'emoji')
EMOJI_CACHE_PATH = "/srv/zulip-emoji-cache"
EMOJI_SCRIPT_DIR_PATH = os.path.join(ZULIP_PATH, 'tools', 'setup', 'emoji')
NODE_MODULES_PATH = os.path.join(ZULIP_PATH, 'node_modules')
EMOJI_CODES_FILE_TEMPLATE = """\
var emoji_codes = (function () {
var exports = {};
exports.names = %(names)s;
exports.name_to_codepoint = %(name_to_codepoint)s;
exports.codepoint_to_name = %(codepoint_to_name)s;
exports.emoji_catalog = %(emoji_catalog)s;
exports.emoticon_conversions = %(emoticon_conversions)s;
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = emoji_codes;
}
"""
SPRITE_CSS_FILE_TEMPLATE = """\
div.emoji,
span.emoji
{
display: inline-block;
background-image: url('sheet-%(emojiset)s-64.png');
-webkit-background-size: 5200%%;
-moz-background-size: 5200%%;
background-size: 5200%%;
background-repeat: no-repeat;
/* Hide the text. */
text-indent: 100%%;
white-space: nowrap;
overflow: hidden;
}
.emoji-1f419
{
background-image: url('images-google-64/1f419.png') !important;
background-position: 0%% 0%% !important;
background-size: contain !important;
}
%(emoji_positions)s
"""
EMOJI_POS_INFO_TEMPLATE = """\
.emoji-%(codepoint)s {
background-position: %(pos_x)s%% %(pos_y)s%%;
}
"""
# change directory
os.chdir(EMOJI_SCRIPT_DIR_PATH)
if 'TRAVIS' in os.environ:
# In Travis CI, we don't have root access
EMOJI_CACHE_PATH = "/home/travis/zulip-emoji-cache"
def main() -> None:
success_stamp = get_success_stamp()
source_emoji_dump = os.path.dirname(success_stamp)
if not os.path.exists(success_stamp):
print("Dumping emojis ...")
dump_emojis(source_emoji_dump)
open(success_stamp, 'w').close()
print("Using cached emojis from {}".format(source_emoji_dump))
if os.path.lexists(TARGET_EMOJI_DUMP):
os.remove(TARGET_EMOJI_DUMP)
os.symlink(source_emoji_dump, TARGET_EMOJI_DUMP)
def get_success_stamp() -> str:
sha1_hexdigest = generate_sha1sum_emoji(ZULIP_PATH)
return os.path.join(EMOJI_CACHE_PATH, sha1_hexdigest, 'emoji', '.success-stamp')
def generate_sprite_css_files(cache_path: str,
emoji_data: List[Dict[str, Any]],
emojiset: str) -> None:
def get_max_val(field: str, emoji_data: List[Dict[str, Any]]) -> int:
max_val = 0
for emoji_dict in emoji_data:
max_val = max(max_val, emoji_dict[field])
if 'skin_variations' in emoji_dict:
for skin_tone, img_info in emoji_dict['skin_variations'].items():
max_val = max(max_val, img_info[field])
return max_val
# Spritesheet CSS generation code. Spritesheets are squared using
# padding, so we have to take only the maximum of two dimensions.
nrows = get_max_val('sheet_x', emoji_data)
ncols = get_max_val('sheet_y', emoji_data)
max_dim = max(nrows, ncols)
emoji_positions = ""
for emoji in emoji_data:
if emoji["has_img_google"]:
# Why is the test here has_img_google and not
# emoji_is_universal? Because we briefly supported all
# Google emoji (not just the universal ones), we need to
# ensure the spritesheet is setup to correctly display
# those google emoji (in case anyone used them).
emoji_positions += EMOJI_POS_INFO_TEMPLATE % {
'codepoint': get_emoji_code(emoji),
'pos_x': (emoji["sheet_x"] * 100) / max_dim,
'pos_y': (emoji["sheet_y"] * 100) / max_dim,
}
SPRITE_CSS_PATH = os.path.join(cache_path, '%s-sprite.css' % (emojiset,))
sprite_css_file = open(SPRITE_CSS_PATH, 'w')
sprite_css_file.write(SPRITE_CSS_FILE_TEMPLATE % {'emojiset': emojiset,
'emoji_positions': emoji_positions,
})
sprite_css_file.close()
def setup_emoji_farms(cache_path: str, emoji_data: List[Dict[str, Any]]) -> None:
def ensure_emoji_image(emoji_dict: Dict[str, Any],
src_emoji_farm: str,
target_emoji_farm: str) -> None:
# We use individual images from emoji farm for rendering emojis
# in notification messages. We have a custom emoji formatter in
# notifications processing code that converts `span` tags to
# `img` and that logic requires us to have non-qualified
# `emoji_code` as file name for emoji.
emoji_code = get_emoji_code(emoji_dict)
img_file_name = emoji_code + '.png'
src_file = os.path.join(src_emoji_farm, emoji_dict['image'])
dst_file = os.path.join(target_emoji_farm, img_file_name)
shutil.copy2(src_file, dst_file)
def setup_emoji_farm(emojiset: str,
emoji_data: List[Dict[str, Any]],
alt_name: Optional[str]=None) -> None:
# `alt_name` is an optional parameter that we use to avoid duplicating below
# code. It is only used while setting up google-blob emojiset as it is just
# a wrapper for an older version of emoji-datasource package due to which we
# need to use 'google' at some places in this code. It has no meaning for other
# emojisets and is just equivalent to `emojiset`.
alt_name = alt_name or emojiset
# Copy individual emoji images from npm packages.
src_emoji_farm = os.path.join(
NODE_MODULES_PATH, 'emoji-datasource-' + emojiset, 'img', alt_name, '64')
target_emoji_farm = os.path.join(cache_path, 'images-' + emojiset + '-64')
os.makedirs(target_emoji_farm, exist_ok=True)
print("Copying individual image files...")
for emoji_dict in emoji_data:
if emoji_dict['has_img_' + alt_name]:
ensure_emoji_image(emoji_dict, src_emoji_farm, target_emoji_farm)
skin_variations = emoji_dict.get('skin_variations', {})
for skin_tone, img_info in skin_variations.items():
if img_info['has_img_' + alt_name]:
ensure_emoji_image(img_info, src_emoji_farm, target_emoji_farm)
# Copy zulip.png to the emoji farm.
zulip_image = os.path.join(ZULIP_PATH, 'static', 'assets', 'zulip-emoji')
for f in os.listdir(zulip_image):
shutil.copy2(os.path.join(zulip_image, f), target_emoji_farm, follow_symlinks=False)
# Copy spritesheets.
emoji_data_path = os.path.join(NODE_MODULES_PATH, 'emoji-datasource-' + emojiset)
input_sprite_sheet = os.path.join(emoji_data_path, 'img', alt_name, 'sheets-256', '64.png')
output_sprite_sheet = os.path.join(cache_path, 'sheet-%s-64.png' % (emojiset,))
shutil.copyfile(input_sprite_sheet, output_sprite_sheet)
# We hardcode octopus emoji image to Google emojiset's old
# "cute octopus" image. Copy it to the emoji farms.
input_img_file = os.path.join(EMOJI_SCRIPT_DIR_PATH, '1f419.png')
output_img_file = os.path.join(cache_path, 'images-' + emojiset + '-64', '1f419.png')
shutil.copyfile(input_img_file, output_img_file)
generate_sprite_css_files(cache_path, emoji_data, emojiset)
# Setup standard emojisets.
for emojiset in ['google', 'twitter']:
setup_emoji_farm(emojiset, emoji_data)
# Setup old google "blobs" emojiset.
GOOGLE_BLOB_EMOJI_DATA_PATH = os.path.join(NODE_MODULES_PATH,
'emoji-datasource-google-blob',
'emoji.json')
with open(GOOGLE_BLOB_EMOJI_DATA_PATH) as fp:
blob_emoji_data = ujson.load(fp)
setup_emoji_farm('google-blob', blob_emoji_data, 'google')
def setup_old_emoji_farm(cache_path: str,
emoji_map: Dict[str, str],
emoji_data: List[Dict[str, Any]]) -> None:
# Code for setting up old emoji farm.
os.chdir(cache_path)
emoji_cache_path = os.path.join(cache_path, 'images', 'emoji')
unicode_emoji_cache_path = os.path.join(cache_path, 'images', 'emoji', 'unicode')
google_emoji_cache_path = os.path.join(cache_path, 'images-google-64')
os.makedirs(emoji_cache_path, exist_ok=True)
os.makedirs(unicode_emoji_cache_path, exist_ok=True)
# Symlink zulip.png image file.
image_file_path = os.path.join(google_emoji_cache_path, 'zulip.png')
symlink_path = os.path.join(emoji_cache_path, 'zulip.png')
os.symlink(image_file_path, symlink_path)
unicode_symlink_path = os.path.join(unicode_emoji_cache_path, 'zulip.png')
os.symlink(image_file_path, unicode_symlink_path)
for name, codepoint in emoji_map.items():
mapped_codepoint = REMAPPED_EMOJIS.get(codepoint, codepoint)
image_file_path = os.path.join(google_emoji_cache_path, '{}.png'.format(mapped_codepoint))
symlink_path = os.path.join(emoji_cache_path, '{}.png'.format(name))
os.symlink(image_file_path, symlink_path)
try:
# `emoji_map` contains duplicate entries for the same codepoint with different
# names. So creation of symlink for <codepoint>.png may throw `FileExistsError`.
unicode_symlink_path = os.path.join(unicode_emoji_cache_path, '{}.png'.format(codepoint))
os.symlink(image_file_path, unicode_symlink_path)
except FileExistsError:
pass
def generate_map_files(cache_path: str, emoji_catalog: Dict[str, List[str]]) -> None:
# This function generates the various data files consumed by webapp, mobile apps, bugdown etc.
names = emoji_names_for_picker(EMOJI_NAME_MAPS)
codepoint_to_name = generate_codepoint_to_name_map(EMOJI_NAME_MAPS)
name_to_codepoint = generate_name_to_codepoint_map(EMOJI_NAME_MAPS)
EMOJI_CODES_FILE_PATH = os.path.join(cache_path, 'emoji_codes.js')
with open(EMOJI_CODES_FILE_PATH, 'w') as emoji_codes_file:
emoji_codes_file.write(EMOJI_CODES_FILE_TEMPLATE % {
'names': names,
'name_to_codepoint': name_to_codepoint,
'codepoint_to_name': codepoint_to_name,
'emoji_catalog': emoji_catalog,
'emoticon_conversions': EMOTICON_CONVERSIONS,
})
NAME_TO_CODEPOINT_PATH = os.path.join(cache_path, 'name_to_codepoint.json')
with open(NAME_TO_CODEPOINT_PATH, 'w') as name_to_codepoint_file:
name_to_codepoint_file.write(ujson.dumps(name_to_codepoint))
CODEPOINT_TO_NAME_PATH = os.path.join(cache_path, 'codepoint_to_name.json')
with open(CODEPOINT_TO_NAME_PATH, 'w') as codepoint_to_name_file:
codepoint_to_name_file.write(ujson.dumps(codepoint_to_name))
EMOTICON_CONVERSIONS_PATH = os.path.join(cache_path, 'emoticon_conversions.json')
with open(EMOTICON_CONVERSIONS_PATH, 'w') as emoticon_conversions_file:
emoticon_conversions_file.write(ujson.dumps(EMOTICON_CONVERSIONS))
def dump_emojis(cache_path: str) -> None:
with open('emoji_map.json') as emoji_map_file:
emoji_map = ujson.load(emoji_map_file)
# `emoji.json` or any other data file can be sourced from any of the supported
# emojiset packages, they all contain the same data files.
EMOJI_DATA_FILE_PATH = os.path.join(NODE_MODULES_PATH, 'emoji-datasource-google', 'emoji.json')
with open(EMOJI_DATA_FILE_PATH) as emoji_data_file:
emoji_data = ujson.load(emoji_data_file)
emoji_catalog = generate_emoji_catalog(emoji_data, EMOJI_NAME_MAPS)
# Setup emoji farms.
if os.path.exists(cache_path):
shutil.rmtree(cache_path)
setup_emoji_farms(cache_path, emoji_data)
setup_old_emoji_farm(cache_path, emoji_map, emoji_data)
# Generate various map files.
generate_map_files(cache_path, emoji_catalog)
if __name__ == "__main__":
main()