py3: Switch almost all shebang lines to use `python3`.
This causes `upgrade-zulip-from-git`, as well as a no-option run of
`tools/build-release-tarball`, to produce a Zulip install running
Python 3, rather than Python 2. In particular this means that the
virtualenv we create, in which all application code runs, is Python 3.
One shebang line, on `zulip-ec2-configure-interfaces`, explicitly
keeps Python 2, and at least one external ops script, `wal-e`, also
still runs on Python 2. See discussion on the respective previous
commits that made those explicit. There may also be some other
third-party scripts we use, outside of this source tree and running
outside our virtualenv, that still run on Python 2.
2017-08-02 23:15:16 +02:00
|
|
|
#!/usr/bin/env python3
|
2017-01-29 21:33:44 +01:00
|
|
|
#
|
|
|
|
# See docs/emoji.md for a high-level explanation of how this system
|
|
|
|
# works.
|
2017-03-20 15:09:26 +01:00
|
|
|
from __future__ import division, print_function
|
2016-10-20 20:41:39 +02:00
|
|
|
import os
|
|
|
|
import glob
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
2017-03-24 19:38:15 +01:00
|
|
|
import ujson
|
2016-10-20 20:41:39 +02:00
|
|
|
import sys
|
|
|
|
import xml.etree.ElementTree as ET
|
2016-12-08 05:06:51 +01:00
|
|
|
from six import unichr
|
2017-03-03 19:01:52 +01:00
|
|
|
from typing import Dict, Text, Union
|
2016-10-20 20:41:39 +02:00
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
2015-09-25 08:56:36 +02:00
|
|
|
|
2017-05-23 17:15:26 +02:00
|
|
|
from emoji_setup_utils import generate_emoji_catalog, generate_codepoint_to_name_map, \
|
|
|
|
emoji_names_for_picker, EMOJISETS
|
2017-01-26 08:35:23 +01:00
|
|
|
|
2016-10-20 20:41:39 +02:00
|
|
|
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../')
|
|
|
|
sys.path.append(ZULIP_PATH)
|
|
|
|
|
2017-08-19 16:23:30 +02:00
|
|
|
from scripts.lib.zulip_tools import generate_sha1sum_emoji, run
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
AA_SCALE = 8
|
|
|
|
SIZE = (136, 136)
|
|
|
|
BIG_SIZE = tuple([x * AA_SCALE for x in SIZE])
|
|
|
|
|
|
|
|
EMOJI_DUMP_DIR_PATH = os.path.join(ZULIP_PATH, 'var', 'emoji_dump')
|
|
|
|
EMOJI_DUMP_PATH = lambda p: os.path.join(EMOJI_DUMP_DIR_PATH, p)
|
2016-12-28 05:07:10 +01:00
|
|
|
TARGET_EMOJI_DUMP = os.path.join(ZULIP_PATH, 'static', 'generated', 'emoji')
|
2016-10-20 20:41:39 +02:00
|
|
|
EMOJI_CACHE_PATH = "/srv/zulip-emoji-cache"
|
2016-12-28 04:58:41 +01:00
|
|
|
EMOJI_SCRIPT_DIR_PATH = os.path.join(ZULIP_PATH, 'tools', 'setup', 'emoji')
|
2017-05-28 13:11:57 +02:00
|
|
|
EMOJI_DATA_PATH = os.path.join(ZULIP_PATH, 'node_modules', 'emoji-datasource')
|
2016-10-20 20:41:39 +02:00
|
|
|
|
2017-01-26 00:41:53 +01:00
|
|
|
EMOJI_CODES_FILE_TEMPLATE = """\
|
|
|
|
var emoji_codes = (function () {
|
|
|
|
var exports = {};
|
|
|
|
|
|
|
|
exports.names = %(names)s;
|
|
|
|
|
|
|
|
exports.codepoints = %(codepoints)s;
|
|
|
|
|
2017-02-10 21:23:58 +01:00
|
|
|
exports.name_to_codepoint = %(name_to_codepoint)s;
|
|
|
|
|
2017-06-24 20:01:38 +02:00
|
|
|
exports.codepoint_to_name = %(codepoint_to_name)s;
|
|
|
|
|
2017-03-19 09:41:24 +01:00
|
|
|
exports.emoji_catalog = %(emoji_catalog)s;
|
|
|
|
|
2017-05-17 11:03:03 +02:00
|
|
|
exports.patched_css_classes = %(patched_css_classes)s;
|
|
|
|
|
2017-01-26 00:41:53 +01:00
|
|
|
return exports;
|
|
|
|
}());
|
|
|
|
if (typeof module !== 'undefined') {
|
|
|
|
module.exports = emoji_codes;
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
2017-03-20 15:09:26 +01:00
|
|
|
SPRITE_CSS_FILE_TEMPLATE = """\
|
|
|
|
div.emoji,
|
|
|
|
span.emoji
|
|
|
|
{
|
|
|
|
display: inline-block;
|
2017-04-01 17:20:32 +02:00
|
|
|
background-image: url('sheet_%(emojiset)s_32.png');
|
2017-05-28 13:11:57 +02:00
|
|
|
-webkit-background-size: 4900%%;
|
|
|
|
-moz-background-size: 4900%%;
|
|
|
|
background-size: 4900%%;
|
2017-03-20 15:09:26 +01:00
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
|
|
/* Hide the text. */
|
|
|
|
text-indent: 100%%;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
%(emoji_positions)s
|
|
|
|
"""
|
|
|
|
|
|
|
|
EMOJI_POS_INFO_TEMPLATE = """\
|
|
|
|
.emoji-%(codepoint)s {
|
|
|
|
background-position: %(pos_x)s%% %(pos_y)s%%;
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
2017-05-24 19:35:00 +02:00
|
|
|
# change directory
|
2016-10-20 20:41:39 +02:00
|
|
|
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"
|
|
|
|
|
|
|
|
class MissingGlyphError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def color_font(name, code_point, code_point_to_fname_map):
|
2016-12-08 05:06:51 +01:00
|
|
|
# type: (str, str, Dict[int, Union[Text, bytes]]) -> None
|
2016-10-20 20:41:39 +02:00
|
|
|
glyph_name = code_point_to_fname_map[int(code_point, 16)]
|
|
|
|
|
|
|
|
in_name = 'bitmaps/strike0/{}.png'.format(glyph_name)
|
|
|
|
out_name = 'out/unicode/{}.png'.format(code_point)
|
|
|
|
|
Change color of white emojis (:one:, :two:, etc) to blue.
The following emojis are colored white (with a transparent background)
and are hard to read in the context of Zulip. This commit changes their
colors to blue. The emojis are:
hash, zero, one, two, three, four, five, six, seven, eight and nine.
This fixes #1969.
2016-12-16 10:53:22 +01:00
|
|
|
# These emojis are colored white and need to be recolored
|
|
|
|
white_emojis = ['eight', 'five', 'four', 'hash', 'nine', 'one',
|
|
|
|
'seven', 'six', 'three', 'two', 'zero']
|
|
|
|
|
2016-10-20 20:41:39 +02:00
|
|
|
try:
|
Change color of white emojis (:one:, :two:, etc) to blue.
The following emojis are colored white (with a transparent background)
and are hard to read in the context of Zulip. This commit changes their
colors to blue. The emojis are:
hash, zero, one, two, three, four, five, six, seven, eight and nine.
This fixes #1969.
2016-12-16 10:53:22 +01:00
|
|
|
if name in white_emojis:
|
|
|
|
white_emoji_image = Image.open(in_name).convert('RGBA')
|
2017-05-16 19:53:41 +02:00
|
|
|
# Reduced image size for having a 4-pixel dark yellow background
|
|
|
|
# on right and bottom of the image.
|
|
|
|
light_yellow_background = Image.new('RGBA', (124, 124), '#FCC21B')
|
|
|
|
dark_yellow_background = Image.new('RGBA', SIZE, '#F79329')
|
|
|
|
# Paste the image on a light yellow background and the resulting
|
|
|
|
# image on a dark yellow background.
|
|
|
|
light_yellow_background.paste(white_emoji_image, mask=white_emoji_image)
|
|
|
|
dark_yellow_background.paste(light_yellow_background, mask=light_yellow_background)
|
|
|
|
dark_yellow_background.save(in_name)
|
2016-10-20 20:41:39 +02:00
|
|
|
shutil.copyfile(in_name, out_name)
|
|
|
|
except IOError:
|
|
|
|
raise MissingGlyphError('code_point: %r' % (code_point))
|
|
|
|
|
|
|
|
|
|
|
|
def bw_font(name, code_point):
|
|
|
|
# type: (str, str) -> None
|
|
|
|
char = unichr(int(code_point, 16))
|
|
|
|
|
|
|
|
# AndroidEmoji.ttf is from
|
|
|
|
# https://android.googlesource.com/platform/frameworks/base.git/+/master/data/fonts/AndroidEmoji.ttf
|
|
|
|
# commit 07912f876c8639f811b06831465c14c4a3b17663
|
|
|
|
font = ImageFont.truetype('AndroidEmoji.ttf', 65 * AA_SCALE)
|
|
|
|
image = Image.new('RGBA', BIG_SIZE)
|
|
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
draw.text((0, 0), char, font=font, fill='black')
|
|
|
|
image.resize(SIZE, Image.ANTIALIAS).save(
|
|
|
|
'out/unicode/{}.png'.format(code_point), 'PNG'
|
|
|
|
)
|
|
|
|
|
|
|
|
def code_point_to_file_name_map(ttx):
|
2016-12-08 05:06:51 +01:00
|
|
|
# type: (str) -> Dict[int, Union[Text, bytes]]
|
2016-10-20 20:41:39 +02:00
|
|
|
"""Given the NotoColorEmoji.ttx file, parse it to generate a map from
|
|
|
|
codepoint to filename (a la glyph0****.png)
|
|
|
|
"""
|
2016-12-08 05:06:51 +01:00
|
|
|
result = {} # type: Dict[int, Union[Text, bytes]]
|
2016-10-20 20:41:39 +02:00
|
|
|
xml = ET.parse(ttx)
|
2017-08-10 21:57:11 +02:00
|
|
|
cmap = xml.find("*cmap_format_12")
|
|
|
|
assert cmap is not None
|
|
|
|
for elem in cmap:
|
2016-10-20 20:41:39 +02:00
|
|
|
code_point = int(elem.attrib["code"], 16)
|
|
|
|
fname = elem.attrib["name"]
|
|
|
|
result[code_point] = fname
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
# type: () -> None
|
|
|
|
# ttx is in the fonttools pacakge, the -z option is only on master
|
|
|
|
# https://github.com/behdad/fonttools/
|
|
|
|
|
|
|
|
# NotoColorEmoji.tff is from
|
2017-03-01 08:13:37 +01:00
|
|
|
# https://android.googlesource.com/platform/external/noto-fonts/+/master/other/NotoColorEmoji.ttf
|
|
|
|
# Commit ID: 1e75a5582b3fb386725aaa944f32fba71f155588
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
# this is so we don't accidently leave ttx files from previous
|
|
|
|
# runs of this script lying around
|
|
|
|
for fname in glob.glob(EMOJI_DUMP_PATH("*ttx*")):
|
|
|
|
os.remove(fname)
|
|
|
|
|
|
|
|
# check if directory `var/emoji_dump` exists
|
|
|
|
subprocess.check_call(['mkdir', '-p', EMOJI_DUMP_DIR_PATH])
|
|
|
|
success_stamp = get_success_stamp()
|
2017-09-22 08:15:01 +02:00
|
|
|
source_emoji_dump = os.path.dirname(success_stamp)
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
if not os.path.exists(success_stamp):
|
|
|
|
print("Dumping emojis ...")
|
|
|
|
dump_emojis(source_emoji_dump)
|
|
|
|
run(['touch', success_stamp])
|
|
|
|
|
|
|
|
print("Using cached emojis from {}".format(source_emoji_dump))
|
|
|
|
run(['rm', '-rf', TARGET_EMOJI_DUMP])
|
2016-11-03 20:36:56 +01:00
|
|
|
try:
|
|
|
|
os.symlink(
|
|
|
|
source_emoji_dump,
|
|
|
|
TARGET_EMOJI_DUMP
|
|
|
|
)
|
|
|
|
except OSError:
|
|
|
|
print("Error: Unable to create symlinks. Make sure you have permission to create symbolic links.")
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
def get_success_stamp():
|
|
|
|
# type: () -> str
|
2017-08-19 16:23:30 +02:00
|
|
|
sha1_hexdigest = generate_sha1sum_emoji(ZULIP_PATH)
|
|
|
|
return os.path.join(EMOJI_CACHE_PATH, sha1_hexdigest, 'emoji', '.success-stamp')
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
def dump_emojis(cache_path):
|
|
|
|
# type: (str) -> None
|
|
|
|
subprocess.call('ttx -v -z extfile -d {} NotoColorEmoji.ttf'.format(EMOJI_DUMP_DIR_PATH), shell=True)
|
|
|
|
|
2017-06-29 23:22:29 +02:00
|
|
|
with open('emoji_map.json') as emoji_map_file:
|
|
|
|
emoji_map = ujson.load(emoji_map_file)
|
|
|
|
|
2016-10-20 20:41:39 +02:00
|
|
|
code_point_to_fname_map = code_point_to_file_name_map(EMOJI_DUMP_PATH("NotoColorEmoji.ttx"))
|
2017-06-29 23:22:29 +02:00
|
|
|
|
|
|
|
EMOJI_DATA_FILE_PATH = os.path.join(EMOJI_DATA_PATH, 'emoji.json')
|
|
|
|
with open(EMOJI_DATA_FILE_PATH) as emoji_data_file:
|
|
|
|
emoji_data = ujson.load(emoji_data_file)
|
2017-03-19 09:41:24 +01:00
|
|
|
emoji_catalog = generate_emoji_catalog(emoji_data)
|
2016-10-20 20:41:39 +02:00
|
|
|
|
2017-05-23 17:15:26 +02:00
|
|
|
UNIFIED_REACTIONS_PATH = os.path.join(ZULIP_PATH, 'zerver', 'management', 'data', 'unified_reactions.json')
|
|
|
|
with open(UNIFIED_REACTIONS_PATH) as unified_reactions_file:
|
|
|
|
unified_reactions_data = ujson.load(unified_reactions_file)
|
|
|
|
|
2016-10-20 20:41:39 +02:00
|
|
|
os.chdir(EMOJI_DUMP_DIR_PATH)
|
|
|
|
|
|
|
|
try:
|
|
|
|
shutil.rmtree('out')
|
|
|
|
except OSError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
os.mkdir('out')
|
|
|
|
os.mkdir('out/unicode')
|
|
|
|
|
|
|
|
failed = False
|
|
|
|
for name, code_point in emoji_map.items():
|
|
|
|
try:
|
|
|
|
color_font(name, code_point, code_point_to_fname_map)
|
|
|
|
except MissingGlyphError:
|
|
|
|
print("Warning: Missing color glyph for %s; using black/white." % (name,))
|
|
|
|
try:
|
|
|
|
bw_font(name, code_point)
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
print('Missing {}, {}'.format(name, code_point))
|
|
|
|
failed = True
|
|
|
|
continue
|
|
|
|
|
2016-11-03 20:36:56 +01:00
|
|
|
try:
|
|
|
|
os.symlink(
|
|
|
|
'unicode/{}.png'.format(code_point),
|
|
|
|
'out/{}.png'.format(name)
|
|
|
|
)
|
|
|
|
except OSError:
|
|
|
|
print("Error: Unable to create symlinks. Make sure you have permission to create symbolic links.")
|
|
|
|
failed = True
|
|
|
|
# probably should not try to create additional links
|
|
|
|
break
|
2016-10-20 20:41:39 +02:00
|
|
|
|
|
|
|
if failed:
|
|
|
|
print("Errors dumping emoji!")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
cache_emoji = os.path.join(cache_path, 'images', 'emoji')
|
2017-02-07 23:34:39 +01:00
|
|
|
cache_emoji_unicode = os.path.join(cache_path, 'images', 'emoji', 'unicode')
|
2016-10-19 05:03:55 +02:00
|
|
|
run(['rm', '-rf', cache_path])
|
|
|
|
run(['mkdir', '-p', cache_emoji])
|
2016-10-20 20:41:39 +02:00
|
|
|
run(['mv', 'out/*', cache_emoji], shell=True)
|
|
|
|
assets = "{}/static/assets/zulip-emoji/*".format(ZULIP_PATH)
|
2017-02-07 23:34:39 +01:00
|
|
|
run(['cp', '-RPp', assets, cache_emoji_unicode], shell=True)
|
|
|
|
|
|
|
|
for fn in [os.path.basename(file_name) for file_name in glob.glob(assets)]:
|
|
|
|
os.symlink(
|
|
|
|
os.path.join(cache_emoji_unicode, fn),
|
|
|
|
os.path.join(cache_emoji, fn)
|
|
|
|
)
|
|
|
|
|
2017-04-01 17:20:32 +02:00
|
|
|
# Spritesheet CSS generation code.
|
2017-03-20 15:09:26 +01:00
|
|
|
emoji_positions = ""
|
|
|
|
for emoji in emoji_data:
|
|
|
|
if emoji["has_img_google"]:
|
2017-05-03 22:01:53 +02:00
|
|
|
emoji_positions += EMOJI_POS_INFO_TEMPLATE % {
|
|
|
|
'codepoint': emoji['unified'].lower(),
|
2017-05-28 13:11:57 +02:00
|
|
|
'pos_x': (emoji["sheet_x"] * 100) / 48,
|
|
|
|
'pos_y': (emoji["sheet_y"] * 100) / 48,
|
2017-05-03 22:01:53 +02:00
|
|
|
}
|
2017-05-16 19:56:41 +02:00
|
|
|
# Remove the code below once the migration to iamcal's dataset is complete.
|
|
|
|
emoji_name = emoji['short_name']
|
|
|
|
codepoint = emoji['unified'].lower()
|
|
|
|
if emoji_name in emoji_map and codepoint != emoji_map[emoji_name]:
|
|
|
|
emoji_positions += EMOJI_POS_INFO_TEMPLATE % {
|
|
|
|
'codepoint': emoji_map[emoji_name],
|
2017-05-28 13:11:57 +02:00
|
|
|
'pos_x': (emoji["sheet_x"] * 100) / 48,
|
|
|
|
'pos_y': (emoji["sheet_y"] * 100) / 48,
|
2017-05-16 19:56:41 +02:00
|
|
|
}
|
|
|
|
|
2017-04-01 17:20:32 +02:00
|
|
|
for emojiset in EMOJISETS:
|
2017-05-28 13:11:57 +02:00
|
|
|
input_sprite_sheet = os.path.join(EMOJI_DATA_PATH, 'img', emojiset, 'sheets', '32.png')
|
|
|
|
output_sprite_sheet = os.path.join(cache_path, 'sheet_%s_32.png' % (emojiset,))
|
|
|
|
run(['cp', input_sprite_sheet, output_sprite_sheet], shell=True)
|
2017-04-01 17:20:32 +02:00
|
|
|
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()
|
2017-03-20 15:09:26 +01:00
|
|
|
|
2017-01-26 00:41:53 +01:00
|
|
|
EMOJI_CODES_PATH = os.path.join(cache_path, 'emoji_codes.js')
|
|
|
|
emoji_codes_file = open(EMOJI_CODES_PATH, 'w')
|
2017-01-29 07:54:42 +01:00
|
|
|
|
|
|
|
# put thumbs_up before thumbs_down
|
|
|
|
names = emoji_names_for_picker(emoji_map)
|
|
|
|
down_index = names.index('thumbs_down')
|
|
|
|
up_index = names.index('thumbs_up')
|
|
|
|
names[down_index], names[up_index] = ('thumbs_up', 'thumbs_down')
|
|
|
|
|
2017-05-17 11:03:03 +02:00
|
|
|
# Patch CSS classes of flag emojis.
|
|
|
|
patched_css_classes = {}
|
|
|
|
for emoji in emoji_data:
|
|
|
|
if emoji['category'] == 'Flags':
|
|
|
|
for name in emoji['short_names']:
|
|
|
|
if name in emoji_map:
|
|
|
|
patched_css_classes[str(name)] = str(emoji['unified'].lower())
|
|
|
|
|
2017-06-24 20:01:38 +02:00
|
|
|
codepoint_to_name = generate_codepoint_to_name_map(names, unified_reactions_data)
|
|
|
|
|
2017-01-26 00:41:53 +01:00
|
|
|
emoji_codes_file.write(EMOJI_CODES_FILE_TEMPLATE % {
|
2017-01-29 07:54:42 +01:00
|
|
|
'names': names,
|
2017-02-10 21:23:58 +01:00
|
|
|
'codepoints': sorted([str(code_point) for code_point in set(emoji_map.values())]),
|
2017-03-19 09:41:24 +01:00
|
|
|
'name_to_codepoint': {str(key): str(emoji_map[key]) for key in emoji_map},
|
2017-06-24 20:01:38 +02:00
|
|
|
'codepoint_to_name': codepoint_to_name,
|
2017-05-17 11:03:03 +02:00
|
|
|
'emoji_catalog': emoji_catalog,
|
|
|
|
'patched_css_classes': patched_css_classes
|
2017-01-26 00:41:53 +01:00
|
|
|
})
|
|
|
|
emoji_codes_file.close()
|
|
|
|
|
2017-02-04 22:44:32 +01:00
|
|
|
NAME_TO_CODEPOINT_PATH = os.path.join(cache_path, 'name_to_codepoint.json')
|
|
|
|
name_to_codepoint_file = open(NAME_TO_CODEPOINT_PATH, 'w')
|
|
|
|
|
2017-03-24 19:38:15 +01:00
|
|
|
name_to_codepoint_file.write(ujson.dumps(emoji_map))
|
2017-02-04 22:44:32 +01:00
|
|
|
name_to_codepoint_file.close()
|
|
|
|
|
2017-05-23 17:15:26 +02:00
|
|
|
CODEPOINT_TO_NAME_PATH = os.path.join(cache_path, 'codepoint_to_name.json')
|
|
|
|
codepoint_to_name_file = open(CODEPOINT_TO_NAME_PATH, 'w')
|
|
|
|
|
|
|
|
codepoint_to_name_file.write(ujson.dumps(codepoint_to_name))
|
|
|
|
codepoint_to_name_file.close()
|
|
|
|
|
2016-10-20 20:41:39 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|