Upgrade to Python 3

This commit is contained in:
Constantin A 2019-11-19 20:05:00 +01:00
parent eea78738cd
commit 0597775dd7
30 changed files with 5599 additions and 5488 deletions

View file

@ -19,6 +19,7 @@ CFLAGS = -std=c99 -Wall -Wextra `pkg-config --cflags --libs cairo`
LDFLAGS = -lm `pkg-config --libs cairo` LDFLAGS = -lm `pkg-config --libs cairo`
PNGQUANTDIR := third_party/pngquant PNGQUANTDIR := third_party/pngquant
PNGQUANT := $(PNGQUANTDIR)/pngquant PNGQUANT := $(PNGQUANTDIR)/pngquant
PYTHON = python3
PNGQUANTFLAGS = --speed 1 --skip-if-larger --quality 85-95 --force PNGQUANTFLAGS = --speed 1 --skip-if-larger --quality 85-95 --force
BODY_DIMENSIONS = 136x128 BODY_DIMENSIONS = 136x128
IMOPS := -size $(BODY_DIMENSIONS) canvas:none -compose copy -gravity center IMOPS := -size $(BODY_DIMENSIONS) canvas:none -compose copy -gravity center
@ -30,12 +31,14 @@ ZOPFLIPNG = zopflipng
OPTIPNG = optipng OPTIPNG = optipng
EMOJI_BUILDER = third_party/color_emoji/emoji_builder.py EMOJI_BUILDER = third_party/color_emoji/emoji_builder.py
# flag for emoji builder. Default to legacy small metrics for the time being.
SMALL_METRICS := -S
ADD_GLYPHS = add_glyphs.py ADD_GLYPHS = add_glyphs.py
ADD_GLYPHS_FLAGS = -a emoji_aliases.txt ADD_GLYPHS_FLAGS = -a emoji_aliases.txt
PUA_ADDER = map_pua_emoji.py PUA_ADDER = map_pua_emoji.py
VS_ADDER = add_vs_cmap.py # from nototools VS_ADDER = add_vs_cmap.py # from nototools
EMOJI_SRC_DIR := png/128 EMOJI_SRC_DIR ?= png/128
FLAGS_SRC_DIR := third_party/region-flags/png FLAGS_SRC_DIR := third_party/region-flags/png
BUILD_DIR := build BUILD_DIR := build
@ -98,7 +101,7 @@ FLAG_NAMES = $(FLAGS:%=%.png)
FLAG_FILES = $(addprefix $(FLAGS_DIR)/, $(FLAG_NAMES)) FLAG_FILES = $(addprefix $(FLAGS_DIR)/, $(FLAG_NAMES))
RESIZED_FLAG_FILES = $(addprefix $(RESIZED_FLAGS_DIR)/, $(FLAG_NAMES)) RESIZED_FLAG_FILES = $(addprefix $(RESIZED_FLAGS_DIR)/, $(FLAG_NAMES))
FLAG_GLYPH_NAMES = $(shell ./flag_glyph_name.py $(FLAGS)) FLAG_GLYPH_NAMES = $(shell $(PYTHON) flag_glyph_name.py $(FLAGS))
RENAMED_FLAG_NAMES = $(FLAG_GLYPH_NAMES:%=emoji_%.png) RENAMED_FLAG_NAMES = $(FLAG_GLYPH_NAMES:%=emoji_%.png)
RENAMED_FLAG_FILES = $(addprefix $(RENAMED_FLAGS_DIR)/, $(RENAMED_FLAG_NAMES)) RENAMED_FLAG_FILES = $(addprefix $(RENAMED_FLAGS_DIR)/, $(RENAMED_FLAG_NAMES))
@ -219,7 +222,7 @@ endif
# Run make without -j if this happens. # Run make without -j if this happens.
%.ttx: %.ttx.tmpl $(ADD_GLYPHS) $(ALL_COMPRESSED_FILES) %.ttx: %.ttx.tmpl $(ADD_GLYPHS) $(ALL_COMPRESSED_FILES)
@python $(ADD_GLYPHS) -f "$<" -o "$@" -d "$(COMPRESSED_DIR)" $(ADD_GLYPHS_FLAGS) @$(PYTHON) $(ADD_GLYPHS) -f "$<" -o "$@" -d "$(COMPRESSED_DIR)" $(ADD_GLYPHS_FLAGS)
%.ttf: %.ttx %.ttf: %.ttx
@rm -f "$@" @rm -f "$@"
@ -227,8 +230,8 @@ endif
$(EMOJI).ttf: $(EMOJI).tmpl.ttf $(EMOJI_BUILDER) $(PUA_ADDER) \ $(EMOJI).ttf: $(EMOJI).tmpl.ttf $(EMOJI_BUILDER) $(PUA_ADDER) \
$(ALL_COMPRESSED_FILES) | check_vs_adder $(ALL_COMPRESSED_FILES) | check_vs_adder
@python $(EMOJI_BUILDER) -V $< "$@" "$(COMPRESSED_DIR)/emoji_u" @$(PYTHON) $(EMOJI_BUILDER) $(SMALL_METRICS) -V $< "$@" "$(COMPRESSED_DIR)/emoji_u"
@python $(PUA_ADDER) "$@" "$@-with-pua" @$(PYTHON) $(PUA_ADDER) "$@" "$@-with-pua"
@$(VS_ADDER) -vs 2640 2642 2695 --dstdir '.' -o "$@-with-pua-varsel" "$@-with-pua" @$(VS_ADDER) -vs 2640 2642 2695 --dstdir '.' -o "$@-with-pua-varsel" "$@-with-pua"
@mv "$@-with-pua-varsel" "$@" @mv "$@-with-pua-varsel" "$@"
@rm "$@-with-pua" @rm "$@-with-pua"

View file

@ -1,224 +1,224 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2017 Google Inc. All rights reserved. # Copyright 2017 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import glob import glob
import os import os
from os import path from os import path
import shutil import shutil
import sys import sys
from nototools import unicode_data from nototools import unicode_data
"""Create aliases in target directory. """Create aliases in target directory.
In addition to links/copies named with aliased sequences, this can also In addition to links/copies named with aliased sequences, this can also
create canonically named aliases/copies, if requested.""" create canonically named aliases/copies, if requested."""
DATA_ROOT = path.dirname(path.abspath(__file__)) DATA_ROOT = path.dirname(path.abspath(__file__))
def str_to_seq(seq_str): def str_to_seq(seq_str):
res = [int(s, 16) for s in seq_str.split('_')] res = [int(s, 16) for s in seq_str.split('_')]
if 0xfe0f in res: if 0xfe0f in res:
print('0xfe0f in file name: %s' % seq_str) print('0xfe0f in file name: %s' % seq_str)
res = [x for x in res if x != 0xfe0f] res = [x for x in res if x != 0xfe0f]
return tuple(res) return tuple(res)
def seq_to_str(seq): def seq_to_str(seq):
return '_'.join('%04x' % cp for cp in seq) return '_'.join('%04x' % cp for cp in seq)
def read_default_unknown_flag_aliases(): def read_default_unknown_flag_aliases():
unknown_flag_path = path.join(DATA_ROOT, 'unknown_flag_aliases.txt') unknown_flag_path = path.join(DATA_ROOT, 'unknown_flag_aliases.txt')
return read_emoji_aliases(unknown_flag_path) return read_emoji_aliases(unknown_flag_path)
def read_default_emoji_aliases(): def read_default_emoji_aliases():
alias_path = path.join(DATA_ROOT, 'emoji_aliases.txt') alias_path = path.join(DATA_ROOT, 'emoji_aliases.txt')
return read_emoji_aliases(alias_path) return read_emoji_aliases(alias_path)
def read_emoji_aliases(filename): def read_emoji_aliases(filename):
result = {} result = {}
with open(filename, 'r') as f: with open(filename, 'r') as f:
for line in f: for line in f:
ix = line.find('#') ix = line.find('#')
if (ix > -1): if (ix > -1):
line = line[:ix] line = line[:ix]
line = line.strip() line = line.strip()
if not line: if not line:
continue continue
als, trg = (s.strip() for s in line.split(';')) als, trg = (s.strip() for s in line.split(';'))
try: try:
als_seq = tuple([int(x, 16) for x in als.split('_')]) als_seq = tuple([int(x, 16) for x in als.split('_')])
trg_seq = tuple([int(x, 16) for x in trg.split('_')]) trg_seq = tuple([int(x, 16) for x in trg.split('_')])
except: except:
print('cannot process alias %s -> %s' % (als, trg)) print('cannot process alias %s -> %s' % (als, trg))
continue continue
result[als_seq] = trg_seq result[als_seq] = trg_seq
return result return result
def add_aliases( def add_aliases(
srcdir, dstdir, aliasfile, prefix, ext, replace=False, copy=False, srcdir, dstdir, aliasfile, prefix, ext, replace=False, copy=False,
canonical_names=False, dry_run=False): canonical_names=False, dry_run=False):
"""Use aliasfile to create aliases of files in srcdir matching prefix/ext in """Use aliasfile to create aliases of files in srcdir matching prefix/ext in
dstdir. If dstdir is null, use srcdir as dstdir. If replace is false dstdir. If dstdir is null, use srcdir as dstdir. If replace is false
and a file already exists in dstdir, report and do nothing. If copy is false and a file already exists in dstdir, report and do nothing. If copy is false
create a symlink, else create a copy. create a symlink, else create a copy.
If canonical_names is true, check all source files and generate aliases/copies If canonical_names is true, check all source files and generate aliases/copies
using the canonical name if different from the existing name. using the canonical name if different from the existing name.
If dry_run is true, report what would be done. Dstdir will be created if If dry_run is true, report what would be done. Dstdir will be created if
necessary, even if dry_run is true.""" necessary, even if dry_run is true."""
if not path.isdir(srcdir): if not path.isdir(srcdir):
print('%s is not a directory' % srcdir, file=sys.stderr) print('%s is not a directory' % srcdir, file=sys.stderr)
return return
if not dstdir: if not dstdir:
dstdir = srcdir dstdir = srcdir
elif not path.isdir(dstdir): elif not path.isdir(dstdir):
os.makedirs(dstdir) os.makedirs(dstdir)
prefix_len = len(prefix) prefix_len = len(prefix)
suffix_len = len(ext) + 1 suffix_len = len(ext) + 1
filenames = [path.basename(f) filenames = [path.basename(f)
for f in glob.glob(path.join(srcdir, '%s*.%s' % (prefix, ext)))] for f in glob.glob(path.join(srcdir, '%s*.%s' % (prefix, ext)))]
seq_to_file = { seq_to_file = {
str_to_seq(name[prefix_len:-suffix_len]) : name str_to_seq(name[prefix_len:-suffix_len]) : name
for name in filenames} for name in filenames}
aliases = read_emoji_aliases(aliasfile) aliases = read_emoji_aliases(aliasfile)
aliases_to_create = {} aliases_to_create = {}
aliases_to_replace = [] aliases_to_replace = []
alias_exists = False alias_exists = False
def check_alias_seq(seq): def check_alias_seq(seq):
alias_str = seq_to_str(seq) alias_str = seq_to_str(seq)
alias_name = '%s%s.%s' % (prefix, alias_str, ext) alias_name = '%s%s.%s' % (prefix, alias_str, ext)
alias_path = path.join(dstdir, alias_name) alias_path = path.join(dstdir, alias_name)
if path.exists(alias_path): if path.exists(alias_path):
if replace: if replace:
aliases_to_replace.append(alias_name) aliases_to_replace.append(alias_name)
else: else:
print('alias %s exists' % alias_str, file=sys.stderr) print('alias %s exists' % alias_str, file=sys.stderr)
alias_exists = True alias_exists = True
return None return None
return alias_name return alias_name
canonical_to_file = {} canonical_to_file = {}
for als, trg in sorted(aliases.items()): for als, trg in sorted(aliases.items()):
if trg not in seq_to_file: if trg not in seq_to_file:
print('target %s for %s does not exist' % ( print('target %s for %s does not exist' % (
seq_to_str(trg), seq_to_str(als)), file=sys.stderr) seq_to_str(trg), seq_to_str(als)), file=sys.stderr)
continue continue
alias_name = check_alias_seq(als) alias_name = check_alias_seq(als)
if alias_name: if alias_name:
target_file = seq_to_file[trg] target_file = seq_to_file[trg]
aliases_to_create[alias_name] = target_file aliases_to_create[alias_name] = target_file
if canonical_names: if canonical_names:
canonical_seq = unicode_data.get_canonical_emoji_sequence(als) canonical_seq = unicode_data.get_canonical_emoji_sequence(als)
if canonical_seq and canonical_seq != als: if canonical_seq and canonical_seq != als:
canonical_alias_name = check_alias_seq(canonical_seq) canonical_alias_name = check_alias_seq(canonical_seq)
if canonical_alias_name: if canonical_alias_name:
canonical_to_file[canonical_alias_name] = target_file canonical_to_file[canonical_alias_name] = target_file
if canonical_names: if canonical_names:
print('adding %d canonical aliases' % len(canonical_to_file)) print('adding %d canonical aliases' % len(canonical_to_file))
for seq, f in seq_to_file.iteritems(): for seq, f in seq_to_file.iteritems():
canonical_seq = unicode_data.get_canonical_emoji_sequence(seq) canonical_seq = unicode_data.get_canonical_emoji_sequence(seq)
if canonical_seq and canonical_seq != seq: if canonical_seq and canonical_seq != seq:
alias_name = check_alias_seq(canonical_seq) alias_name = check_alias_seq(canonical_seq)
if alias_name: if alias_name:
canonical_to_file[alias_name] = f canonical_to_file[alias_name] = f
print('adding %d total canonical sequences' % len(canonical_to_file)) print('adding %d total canonical sequences' % len(canonical_to_file))
aliases_to_create.update(canonical_to_file) aliases_to_create.update(canonical_to_file)
if replace: if replace:
if not dry_run: if not dry_run:
for k in sorted(aliases_to_replace): for k in sorted(aliases_to_replace):
os.remove(path.join(dstdir, k)) os.remove(path.join(dstdir, k))
print('replacing %d files' % len(aliases_to_replace)) print('replacing %d files' % len(aliases_to_replace))
elif alias_exists: elif alias_exists:
print('aborting, aliases exist.', file=sys.stderr) print('aborting, aliases exist.', file=sys.stderr)
return return
for k, v in sorted(aliases_to_create.items()): for k, v in sorted(aliases_to_create.items()):
if dry_run: if dry_run:
msg = 'replace ' if k in aliases_to_replace else '' msg = 'replace ' if k in aliases_to_replace else ''
print('%s%s -> %s' % (msg, k, v)) print('%s%s -> %s' % (msg, k, v))
else: else:
try: try:
if copy: if copy:
shutil.copy2(path.join(srcdir, v), path.join(dstdir, k)) shutil.copy2(path.join(srcdir, v), path.join(dstdir, k))
else: else:
# fix this to create relative symlinks # fix this to create relative symlinks
if srcdir == dstdir: if srcdir == dstdir:
os.symlink(v, path.join(dstdir, k)) os.symlink(v, path.join(dstdir, k))
else: else:
raise Exception('can\'t create cross-directory symlinks yet') raise Exception('can\'t create cross-directory symlinks yet')
except Exception as e: except Exception as e:
print('failed to create %s -> %s' % (k, v), file=sys.stderr) print('failed to create %s -> %s' % (k, v), file=sys.stderr)
raise Exception('oops, ' + str(e)) raise Exception('oops, ' + str(e))
print('created %d %s' % ( print('created %d %s' % (
len(aliases_to_create), 'copies' if copy else 'symlinks')) len(aliases_to_create), 'copies' if copy else 'symlinks'))
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-s', '--srcdir', help='directory containing files to alias', '-s', '--srcdir', help='directory containing files to alias',
required=True, metavar='dir') required=True, metavar='dir')
parser.add_argument( parser.add_argument(
'-d', '--dstdir', help='directory to write aliases, default srcdir', '-d', '--dstdir', help='directory to write aliases, default srcdir',
metavar='dir') metavar='dir')
parser.add_argument( parser.add_argument(
'-a', '--aliasfile', help='alias file (default emoji_aliases.txt)', '-a', '--aliasfile', help='alias file (default emoji_aliases.txt)',
metavar='file', default='emoji_aliases.txt') metavar='file', default='emoji_aliases.txt')
parser.add_argument( parser.add_argument(
'-p', '--prefix', help='file name prefix (default emoji_u)', '-p', '--prefix', help='file name prefix (default emoji_u)',
metavar='pfx', default='emoji_u') metavar='pfx', default='emoji_u')
parser.add_argument( parser.add_argument(
'-e', '--ext', help='file name extension (default png)', '-e', '--ext', help='file name extension (default png)',
choices=['ai', 'png', 'svg'], default='png') choices=['ai', 'png', 'svg'], default='png')
parser.add_argument( parser.add_argument(
'-r', '--replace', help='replace existing files/aliases', '-r', '--replace', help='replace existing files/aliases',
action='store_true') action='store_true')
parser.add_argument( parser.add_argument(
'-c', '--copy', help='create a copy of the file, not a symlink', '-c', '--copy', help='create a copy of the file, not a symlink',
action='store_true') action='store_true')
parser.add_argument( parser.add_argument(
'--canonical_names', help='include extra copies with canonical names ' '--canonical_names', help='include extra copies with canonical names '
'(including fe0f emoji presentation character)', action='store_true'); '(including fe0f emoji presentation character)', action='store_true');
parser.add_argument( parser.add_argument(
'-n', '--dry_run', help='print out aliases to create only', '-n', '--dry_run', help='print out aliases to create only',
action='store_true') action='store_true')
args = parser.parse_args() args = parser.parse_args()
add_aliases( add_aliases(
args.srcdir, args.dstdir, args.aliasfile, args.prefix, args.ext, args.srcdir, args.dstdir, args.aliasfile, args.prefix, args.ext,
args.replace, args.copy, args.canonical_names, args.dry_run) args.replace, args.copy, args.canonical_names, args.dry_run)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,195 +1,195 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2014 Google Inc. All rights reserved. # Copyright 2014 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Modify the Noto Color Emoji font to use GSUB rules for flags and keycaps.""" """Modify the Noto Color Emoji font to use GSUB rules for flags and keycaps."""
__author__ = "roozbeh@google.com (Roozbeh Pournader)" __author__ = "roozbeh@google.com (Roozbeh Pournader)"
import sys import sys
from fontTools import agl from fontTools import agl
from fontTools import ttLib from fontTools import ttLib
from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables import otTables
from nototools import font_data from nototools import font_data
def create_script_list(script_tag='DFLT'): def create_script_list(script_tag='DFLT'):
"""Create a ScriptList for the GSUB table.""" """Create a ScriptList for the GSUB table."""
def_lang_sys = otTables.DefaultLangSys() def_lang_sys = otTables.DefaultLangSys()
def_lang_sys.ReqFeatureIndex = 0xFFFF def_lang_sys.ReqFeatureIndex = 0xFFFF
def_lang_sys.FeatureCount = 1 def_lang_sys.FeatureCount = 1
def_lang_sys.FeatureIndex = [0] def_lang_sys.FeatureIndex = [0]
def_lang_sys.LookupOrder = None def_lang_sys.LookupOrder = None
script_record = otTables.ScriptRecord() script_record = otTables.ScriptRecord()
script_record.ScriptTag = script_tag script_record.ScriptTag = script_tag
script_record.Script = otTables.Script() script_record.Script = otTables.Script()
script_record.Script.DefaultLangSys = def_lang_sys script_record.Script.DefaultLangSys = def_lang_sys
script_record.Script.LangSysCount = 0 script_record.Script.LangSysCount = 0
script_record.Script.LangSysRecord = [] script_record.Script.LangSysRecord = []
script_list = otTables.ScriptList() script_list = otTables.ScriptList()
script_list.ScriptCount = 1 script_list.ScriptCount = 1
script_list.ScriptRecord = [script_record] script_list.ScriptRecord = [script_record]
return script_list return script_list
def create_feature_list(feature_tag, lookup_count): def create_feature_list(feature_tag, lookup_count):
"""Create a FeatureList for the GSUB table.""" """Create a FeatureList for the GSUB table."""
feature_record = otTables.FeatureRecord() feature_record = otTables.FeatureRecord()
feature_record.FeatureTag = feature_tag feature_record.FeatureTag = feature_tag
feature_record.Feature = otTables.Feature() feature_record.Feature = otTables.Feature()
feature_record.Feature.LookupCount = lookup_count feature_record.Feature.LookupCount = lookup_count
feature_record.Feature.LookupListIndex = range(lookup_count) feature_record.Feature.LookupListIndex = range(lookup_count)
feature_record.Feature.FeatureParams = None feature_record.Feature.FeatureParams = None
feature_list = otTables.FeatureList() feature_list = otTables.FeatureList()
feature_list.FeatureCount = 1 feature_list.FeatureCount = 1
feature_list.FeatureRecord = [feature_record] feature_list.FeatureRecord = [feature_record]
return feature_list return feature_list
def create_lookup_list(lookups): def create_lookup_list(lookups):
"""Create a LookupList for the GSUB table.""" """Create a LookupList for the GSUB table."""
lookup_list = otTables.LookupList() lookup_list = otTables.LookupList()
lookup_list.LookupCount = len(lookups) lookup_list.LookupCount = len(lookups)
lookup_list.Lookup = lookups lookup_list.Lookup = lookups
return lookup_list return lookup_list
def get_glyph_name_or_create(char, font): def get_glyph_name_or_create(char, font):
"""Return the glyph name for a character, creating if it doesn't exist.""" """Return the glyph name for a character, creating if it doesn't exist."""
cmap = font_data.get_cmap(font) cmap = font_data.get_cmap(font)
if char in cmap: if char in cmap:
return cmap[char] return cmap[char]
glyph_name = agl.UV2AGL[char] glyph_name = agl.UV2AGL[char]
assert glyph_name not in font.glyphOrder assert glyph_name not in font.glyphOrder
font['hmtx'].metrics[glyph_name] = [0, 0] font['hmtx'].metrics[glyph_name] = [0, 0]
cmap[char] = glyph_name cmap[char] = glyph_name
if 'glyf' in font: if 'glyf' in font:
from fontTools.ttLib.tables import _g_l_y_f from fontTools.ttLib.tables import _g_l_y_f
empty_glyph = _g_l_y_f.Glyph() empty_glyph = _g_l_y_f.Glyph()
font['glyf'].glyphs[glyph_name] = empty_glyph font['glyf'].glyphs[glyph_name] = empty_glyph
font.glyphOrder.append(glyph_name) font.glyphOrder.append(glyph_name)
return glyph_name return glyph_name
def create_lookup(table, font, flag=0): def create_lookup(table, font, flag=0):
"""Create a Lookup based on mapping table.""" """Create a Lookup based on mapping table."""
cmap = font_data.get_cmap(font) cmap = font_data.get_cmap(font)
ligatures = {} ligatures = {}
for output, (ch1, ch2) in table.iteritems(): for output, (ch1, ch2) in table.iteritems():
output = cmap[output] output = cmap[output]
ch1 = get_glyph_name_or_create(ch1, font) ch1 = get_glyph_name_or_create(ch1, font)
ch2 = get_glyph_name_or_create(ch2, font) ch2 = get_glyph_name_or_create(ch2, font)
ligature = otTables.Ligature() ligature = otTables.Ligature()
ligature.CompCount = 2 ligature.CompCount = 2
ligature.Component = [ch2] ligature.Component = [ch2]
ligature.LigGlyph = output ligature.LigGlyph = output
try: try:
ligatures[ch1].append(ligature) ligatures[ch1].append(ligature)
except KeyError: except KeyError:
ligatures[ch1] = [ligature] ligatures[ch1] = [ligature]
ligature_subst = otTables.LigatureSubst() ligature_subst = otTables.LigatureSubst()
ligature_subst.ligatures = ligatures ligature_subst.ligatures = ligatures
lookup = otTables.Lookup() lookup = otTables.Lookup()
lookup.LookupType = 4 lookup.LookupType = 4
lookup.LookupFlag = flag lookup.LookupFlag = flag
lookup.SubTableCount = 1 lookup.SubTableCount = 1
lookup.SubTable = [ligature_subst] lookup.SubTable = [ligature_subst]
return lookup return lookup
def create_simple_gsub(lookups, script='DFLT', feature='ccmp'): def create_simple_gsub(lookups, script='DFLT', feature='ccmp'):
"""Create a simple GSUB table.""" """Create a simple GSUB table."""
gsub_class = ttLib.getTableClass('GSUB') gsub_class = ttLib.getTableClass('GSUB')
gsub = gsub_class('GSUB') gsub = gsub_class('GSUB')
gsub.table = otTables.GSUB() gsub.table = otTables.GSUB()
gsub.table.Version = 1.0 gsub.table.Version = 1.0
gsub.table.ScriptList = create_script_list(script) gsub.table.ScriptList = create_script_list(script)
gsub.table.FeatureList = create_feature_list(feature, len(lookups)) gsub.table.FeatureList = create_feature_list(feature, len(lookups))
gsub.table.LookupList = create_lookup_list(lookups) gsub.table.LookupList = create_lookup_list(lookups)
return gsub return gsub
def reg_indicator(letter): def reg_indicator(letter):
"""Return a regional indicator charater from corresponing capital letter. """Return a regional indicator charater from corresponing capital letter.
""" """
return 0x1F1E6 + ord(letter) - ord('A') return 0x1F1E6 + ord(letter) - ord('A')
EMOJI_FLAGS = { EMOJI_FLAGS = {
0xFE4E5: (reg_indicator('J'), reg_indicator('P')), # Japan 0xFE4E5: (reg_indicator('J'), reg_indicator('P')), # Japan
0xFE4E6: (reg_indicator('U'), reg_indicator('S')), # United States 0xFE4E6: (reg_indicator('U'), reg_indicator('S')), # United States
0xFE4E7: (reg_indicator('F'), reg_indicator('R')), # France 0xFE4E7: (reg_indicator('F'), reg_indicator('R')), # France
0xFE4E8: (reg_indicator('D'), reg_indicator('E')), # Germany 0xFE4E8: (reg_indicator('D'), reg_indicator('E')), # Germany
0xFE4E9: (reg_indicator('I'), reg_indicator('T')), # Italy 0xFE4E9: (reg_indicator('I'), reg_indicator('T')), # Italy
0xFE4EA: (reg_indicator('G'), reg_indicator('B')), # United Kingdom 0xFE4EA: (reg_indicator('G'), reg_indicator('B')), # United Kingdom
0xFE4EB: (reg_indicator('E'), reg_indicator('S')), # Spain 0xFE4EB: (reg_indicator('E'), reg_indicator('S')), # Spain
0xFE4EC: (reg_indicator('R'), reg_indicator('U')), # Russia 0xFE4EC: (reg_indicator('R'), reg_indicator('U')), # Russia
0xFE4ED: (reg_indicator('C'), reg_indicator('N')), # China 0xFE4ED: (reg_indicator('C'), reg_indicator('N')), # China
0xFE4EE: (reg_indicator('K'), reg_indicator('R')), # Korea 0xFE4EE: (reg_indicator('K'), reg_indicator('R')), # Korea
} }
KEYCAP = 0x20E3 KEYCAP = 0x20E3
EMOJI_KEYCAPS = { EMOJI_KEYCAPS = {
0xFE82C: (ord('#'), KEYCAP), 0xFE82C: (ord('#'), KEYCAP),
0xFE82E: (ord('1'), KEYCAP), 0xFE82E: (ord('1'), KEYCAP),
0xFE82F: (ord('2'), KEYCAP), 0xFE82F: (ord('2'), KEYCAP),
0xFE830: (ord('3'), KEYCAP), 0xFE830: (ord('3'), KEYCAP),
0xFE831: (ord('4'), KEYCAP), 0xFE831: (ord('4'), KEYCAP),
0xFE832: (ord('5'), KEYCAP), 0xFE832: (ord('5'), KEYCAP),
0xFE833: (ord('6'), KEYCAP), 0xFE833: (ord('6'), KEYCAP),
0xFE834: (ord('7'), KEYCAP), 0xFE834: (ord('7'), KEYCAP),
0xFE835: (ord('8'), KEYCAP), 0xFE835: (ord('8'), KEYCAP),
0xFE836: (ord('9'), KEYCAP), 0xFE836: (ord('9'), KEYCAP),
0xFE837: (ord('0'), KEYCAP), 0xFE837: (ord('0'), KEYCAP),
} }
def main(argv): def main(argv):
"""Modify all the fonts given in the command line.""" """Modify all the fonts given in the command line."""
for font_name in argv[1:]: for font_name in argv[1:]:
font = ttLib.TTFont(font_name) font = ttLib.TTFont(font_name)
assert 'GSUB' not in font assert 'GSUB' not in font
font['GSUB'] = create_simple_gsub([ font['GSUB'] = create_simple_gsub([
create_lookup(EMOJI_KEYCAPS, font), create_lookup(EMOJI_KEYCAPS, font),
create_lookup(EMOJI_FLAGS, font)]) create_lookup(EMOJI_FLAGS, font)])
font_data.delete_from_cmap( font_data.delete_from_cmap(
font, EMOJI_FLAGS.keys() + EMOJI_KEYCAPS.keys()) font, EMOJI_FLAGS.keys() + EMOJI_KEYCAPS.keys())
font.save(font_name+'-fixed') font.save(font_name+'-fixed')
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv) main(sys.argv)

View file

@ -1,407 +1,405 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""Extend a ttx file with additional data. """Extend a ttx file with additional data.
Takes a ttx file and one or more directories containing image files named Takes a ttx file and one or more directories containing image files named
after sequences of codepoints, extends the cmap, hmtx, GSUB, and GlyphOrder after sequences of codepoints, extends the cmap, hmtx, GSUB, and GlyphOrder
tables in the source ttx file based on these sequences, and writes out a new tables in the source ttx file based on these sequences, and writes out a new
ttx file. ttx file.
This can also apply aliases from an alias file.""" This can also apply aliases from an alias file."""
import argparse import argparse
import collections import collections
import os import os
from os import path from os import path
import re import re
import sys import sys
from fontTools import ttx from fontTools import ttx
from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables import otTables
import add_emoji_gsub import add_emoji_gsub
import add_aliases import add_aliases
sys.path.append( sys.path.append(
path.join(os.path.dirname(__file__), 'third_party', 'color_emoji')) path.join(os.path.dirname(__file__), 'third_party', 'color_emoji'))
from png import PNG from png import PNG
def get_seq_to_file(image_dir, prefix, suffix): def get_seq_to_file(image_dir, prefix, suffix):
"""Return a mapping from codepoint sequences to files in the given directory, """Return a mapping from codepoint sequences to files in the given directory,
for files that match the prefix and suffix. File names with this prefix and for files that match the prefix and suffix. File names with this prefix and
suffix should consist of codepoints in hex separated by underscore. 'fe0f' suffix should consist of codepoints in hex separated by underscore. 'fe0f'
(the codepoint of the emoji presentation variation selector) is stripped from (the codepoint of the emoji presentation variation selector) is stripped from
the sequence. the sequence.
""" """
start = len(prefix) start = len(prefix)
limit = -len(suffix) limit = -len(suffix)
seq_to_file = {} seq_to_file = {}
for name in os.listdir(image_dir): for name in os.listdir(image_dir):
if not (name.startswith(prefix) and name.endswith(suffix)): if not (name.startswith(prefix) and name.endswith(suffix)):
continue continue
try: try:
cps = [int(s, 16) for s in name[start:limit].split('_')] cps = [int(s, 16) for s in name[start:limit].split('_')]
seq = tuple(cp for cp in cps if cp != 0xfe0f) seq = tuple(cp for cp in cps if cp != 0xfe0f)
except: except:
raise Exception('could not parse "%s"' % name) raise Exception('could not parse "%s"' % name)
for cp in cps: for cp in cps:
if not (0 <= cp <= 0x10ffff): if not (0 <= cp <= 0x10ffff):
raise Exception('bad codepoint(s) in "%s"' % name) raise Exception('bad codepoint(s) in "%s"' % name)
if seq in seq_to_file: if seq in seq_to_file:
raise Exception('duplicate sequence for "%s" in %s' % (name, image_dir)) raise Exception('duplicate sequence for "%s" in %s' % (name, image_dir))
seq_to_file[seq] = path.join(image_dir, name) seq_to_file[seq] = path.join(image_dir, name)
return seq_to_file return seq_to_file
def collect_seq_to_file(image_dirs, prefix, suffix): def collect_seq_to_file(image_dirs, prefix, suffix):
"""Return a sequence to file mapping by calling get_seq_to_file on a list """Return a sequence to file mapping by calling get_seq_to_file on a list
of directories. When sequences for files in later directories match those of directories. When sequences for files in later directories match those
from earlier directories, the later file replaces the earlier one. from earlier directories, the later file replaces the earlier one.
""" """
seq_to_file = {} seq_to_file = {}
for image_dir in image_dirs: for image_dir in image_dirs:
seq_to_file.update(get_seq_to_file(image_dir, prefix, suffix)) seq_to_file.update(get_seq_to_file(image_dir, prefix, suffix))
return seq_to_file return seq_to_file
def remap_values(seq_to_file, map_fn): def remap_values(seq_to_file, map_fn):
return {k: map_fn(v) for k, v in seq_to_file.iteritems()} return {k: map_fn(v) for k, v in seq_to_file.items()}
def get_png_file_to_advance_mapper(lineheight): def get_png_file_to_advance_mapper(lineheight):
def map_fn(filename): def map_fn(filename):
wid, ht = PNG(filename).get_size() wid, ht = PNG(filename).get_size()
return int(round(float(lineheight) * wid / ht)) return int(round(float(lineheight) * wid / ht))
return map_fn return map_fn
def cp_name(cp): def cp_name(cp):
"""return uniXXXX or uXXXXX(X) as a name for the glyph mapped to this cp.""" """return uniXXXX or uXXXXX(X) as a name for the glyph mapped to this cp."""
return '%s%04X' % ('u' if cp > 0xffff else 'uni', cp) return '%s%04X' % ('u' if cp > 0xffff else 'uni', cp)
def seq_name(seq): def seq_name(seq):
"""Sequences of length one get the cp_name. Others start with 'u' followed by """Sequences of length one get the cp_name. Others start with 'u' followed by
two or more 4-to-6-digit hex strings separated by underscore.""" two or more 4-to-6-digit hex strings separated by underscore."""
if len(seq) == 1: if len(seq) == 1:
return cp_name(seq[0]) return cp_name(seq[0])
return 'u' + '_'.join('%04X' % cp for cp in seq) return 'u' + '_'.join('%04X' % cp for cp in seq)
def collect_cps(seqs): def collect_cps(seqs):
cps = set() cps = set()
for seq in seqs: for seq in seqs:
cps.update(seq) cps.update(seq)
return cps return cps
def get_glyphorder_cps_and_truncate(glyphOrder): def get_glyphorder_cps_and_truncate(glyphOrder):
"""This scans glyphOrder for names that correspond to a single codepoint """This scans glyphOrder for names that correspond to a single codepoint
using the 'u(ni)XXXXXX' syntax. All names that don't match are moved using the 'u(ni)XXXXXX' syntax. All names that don't match are moved
to the front the glyphOrder list in their original order, and the to the front the glyphOrder list in their original order, and the
list is truncated. The ones that do match are returned as a set of list is truncated. The ones that do match are returned as a set of
codepoints.""" codepoints."""
glyph_name_re = re.compile(r'^u(?:ni)?([0-9a-fA-F]{4,6})$') glyph_name_re = re.compile(r'^u(?:ni)?([0-9a-fA-F]{4,6})$')
cps = set() cps = set()
write_ix = 0 write_ix = 0
for ix, name in enumerate(glyphOrder): for ix, name in enumerate(glyphOrder):
m = glyph_name_re.match(name) m = glyph_name_re.match(name)
if m: if m:
cps.add(int(m.group(1), 16)) cps.add(int(m.group(1), 16))
else: else:
glyphOrder[write_ix] = name glyphOrder[write_ix] = name
write_ix += 1 write_ix += 1
del glyphOrder[write_ix:] del glyphOrder[write_ix:]
return cps return cps
def get_all_seqs(font, seq_to_advance): def get_all_seqs(font, seq_to_advance):
"""Copies the sequences from seq_to_advance and extends it with single- """Copies the sequences from seq_to_advance and extends it with single-
codepoint sequences from the GlyphOrder table as well as those internal codepoint sequences from the GlyphOrder table as well as those internal
to sequences in seq_to_advance. Reduces the GlyphOrder table. """ to sequences in seq_to_advance. Reduces the GlyphOrder table. """
all_seqs = set(seq_to_advance.keys()) all_seqs = set(seq_to_advance.keys())
# using collect_cps includes cps internal to a seq # using collect_cps includes cps internal to a seq
cps = collect_cps(all_seqs) cps = collect_cps(all_seqs)
glyphOrder = font.getGlyphOrder() glyphOrder = font.getGlyphOrder()
# extract cps in glyphOrder and reduce glyphOrder to only those that remain # extract cps in glyphOrder and reduce glyphOrder to only those that remain
glyphOrder_cps = get_glyphorder_cps_and_truncate(glyphOrder) glyphOrder_cps = get_glyphorder_cps_and_truncate(glyphOrder)
cps.update(glyphOrder_cps) cps.update(glyphOrder_cps)
# add new single codepoint sequences from glyphOrder and sequences # add new single codepoint sequences from glyphOrder and sequences
all_seqs.update((cp,) for cp in cps) all_seqs.update((cp,) for cp in cps)
return all_seqs return all_seqs
def get_font_cmap(font): def get_font_cmap(font):
"""Return the first cmap in the font, we assume it exists and is a unicode """Return the first cmap in the font, we assume it exists and is a unicode
cmap.""" cmap."""
return font['cmap'].tables[0].cmap return font['cmap'].tables[0].cmap
def add_glyph_data(font, seqs, seq_to_advance, vadvance): def add_glyph_data(font, seqs, seq_to_advance, vadvance):
"""Add hmtx and GlyphOrder data for all sequences in seqs, and ensures there's """Add hmtx and GlyphOrder data for all sequences in seqs, and ensures there's
a cmap entry for each single-codepoint sequence. Seqs not in seq_to_advance a cmap entry for each single-codepoint sequence. Seqs not in seq_to_advance
will get a zero advance.""" will get a zero advance."""
# We allow the template cmap to omit mappings for single-codepoint glyphs # We allow the template cmap to omit mappings for single-codepoint glyphs
# defined in the template's GlyphOrder table. Similarly, the hmtx table can # defined in the template's GlyphOrder table. Similarly, the hmtx table can
# omit advances. We assume glyphs named 'uniXXXX' or 'uXXXXX(X)' in the # omit advances. We assume glyphs named 'uniXXXX' or 'uXXXXX(X)' in the
# GlyphOrder table correspond to codepoints based on the name; we don't # GlyphOrder table correspond to codepoints based on the name; we don't
# attempt to handle other types of names and these must occur in the cmap and # attempt to handle other types of names and these must occur in the cmap and
# hmtx tables in the template. # hmtx tables in the template.
# #
# seq_to_advance maps sequences (including single codepoints) to advances. # seq_to_advance maps sequences (including single codepoints) to advances.
# All codepoints in these sequences will be added to the cmap. Some cps # All codepoints in these sequences will be added to the cmap. Some cps
# in these sequences have no corresponding single-codepoint sequence, they # in these sequences have no corresponding single-codepoint sequence, they
# will also get added. # will also get added.
# #
# The added codepoints have no advance information, so will get a zero # The added codepoints have no advance information, so will get a zero
# advance. # advance.
cmap = get_font_cmap(font) cmap = get_font_cmap(font)
hmtx = font['hmtx'].metrics hmtx = font['hmtx'].metrics
vmtx = font['vmtx'].metrics vmtx = font['vmtx'].metrics
# We don't expect sequences to be in the glyphOrder, since we removed all the # We don't expect sequences to be in the glyphOrder, since we removed all the
# single-cp sequences from it and don't expect it to already contain names # single-cp sequences from it and don't expect it to already contain names
# corresponding to multiple-cp sequencess. But just in case, we use # corresponding to multiple-cp sequencess. But just in case, we use
# reverseGlyphMap to avoid duplicating names accidentally. # reverseGlyphMap to avoid duplicating names accidentally.
updatedGlyphOrder = False updatedGlyphOrder = False
reverseGlyphMap = font.getReverseGlyphMap() reverseGlyphMap = font.getReverseGlyphMap()
# Order the glyphs by grouping all the single-codepoint sequences first, # Order the glyphs by grouping all the single-codepoint sequences first,
# then order by sequence so that related sequences are together. We group # then order by sequence so that related sequences are together. We group
# by single-codepoint sequence first in order to keep these glyphs together-- # by single-codepoint sequence first in order to keep these glyphs together--
# they're used in the coverage tables for some of the substitutions, and # they're used in the coverage tables for some of the substitutions, and
# those tables can be more compact this way. # those tables can be more compact this way.
for seq in sorted(seqs, key=lambda s: (0 if len(s) == 1 else 1, s)): for seq in sorted(seqs, key=lambda s: (0 if len(s) == 1 else 1, s)):
name = seq_name(seq) name = seq_name(seq)
if len(seq) == 1: if len(seq) == 1:
cmap[seq[0]] = name cmap[seq[0]] = name
advance = seq_to_advance.get(seq, 0) advance = seq_to_advance.get(seq, 0)
hmtx[name] = [advance, 0] hmtx[name] = [advance, 0]
vmtx[name] = [vadvance, 0] vmtx[name] = [vadvance, 0]
if name not in reverseGlyphMap: if name not in reverseGlyphMap:
font.glyphOrder.append(name) font.glyphOrder.append(name)
updatedGlyphOrder=True updatedGlyphOrder=True
if updatedGlyphOrder: if updatedGlyphOrder:
delattr(font, '_reverseGlyphOrderDict') delattr(font, '_reverseGlyphOrderDict')
def add_aliases_to_cmap(font, aliases): def add_aliases_to_cmap(font, aliases):
"""Some aliases might map a single codepoint to some other sequence. These """Some aliases might map a single codepoint to some other sequence. These
should map directly to the glyph for that sequence in the cmap. (Others will should map directly to the glyph for that sequence in the cmap. (Others will
map via GSUB). map via GSUB).
""" """
if not aliases: if not aliases:
return return
cp_aliases = [seq for seq in aliases if len(seq) == 1] cp_aliases = [seq for seq in aliases if len(seq) == 1]
if not cp_aliases: if not cp_aliases:
return return
cmap = get_font_cmap(font) cmap = get_font_cmap(font)
for src_seq in cp_aliases: for src_seq in cp_aliases:
cp = src_seq[0] cp = src_seq[0]
name = seq_name(aliases[src_seq]) name = seq_name(aliases[src_seq])
cmap[cp] = name cmap[cp] = name
def get_rtl_seq(seq): def get_rtl_seq(seq):
"""Return the rtl variant of the sequence, if it has one, else the empty """Return the rtl variant of the sequence, if it has one, else the empty
sequence. sequence.
""" """
# Sequences with ZWJ or TAG_END in them will reflect. Fitzpatrick modifiers # Sequences with ZWJ or TAG_END in them will reflect. Fitzpatrick modifiers
# however do not, so if we reflect we make a pass to swap them back into their # however do not, so if we reflect we make a pass to swap them back into their
# logical order. # logical order.
ZWJ = 0x200d ZWJ = 0x200d
TAG_END = 0xe007f TAG_END = 0xe007f
def is_fitzpatrick(cp): def is_fitzpatrick(cp):
return 0x1f3fb <= cp <= 0x1f3ff return 0x1f3fb <= cp <= 0x1f3ff
if not (ZWJ in seq or TAG_END in seq): if not (ZWJ in seq or TAG_END in seq):
return () return ()
rev_seq = list(seq) rev_seq = list(seq)
rev_seq.reverse() rev_seq.reverse()
for i in xrange(1, len(rev_seq)): for i in range(len(rev_seq)-1, 0, -1):
if is_fitzpatrick(rev_seq[i-1]): if is_fitzpatrick(rev_seq[i-1]):
tmp = rev_seq[i] rev_seq[i-1], rev_seq[i] = rev_seq[i], rev_seq[i-1]
rev_seq[i] = rev_seq[i-1] return tuple(rev_seq)
rev_seq[i-1] = tmp
return tuple(rev_seq)
def get_gsub_ligature_lookup(font):
"""If the font does not have a GSUB table, create one with a ligature
def get_gsub_ligature_lookup(font): substitution lookup. If it does, ensure the first lookup is a properly
"""If the font does not have a GSUB table, create one with a ligature initialized ligature substitution lookup. Return the lookup."""
substitution lookup. If it does, ensure the first lookup is a properly
initialized ligature substitution lookup. Return the lookup.""" # The template might include more lookups after lookup 0, if it has a
# GSUB table.
# The template might include more lookups after lookup 0, if it has a if 'GSUB' not in font:
# GSUB table. ligature_subst = otTables.LigatureSubst()
if 'GSUB' not in font: ligature_subst.ligatures = {}
ligature_subst = otTables.LigatureSubst()
ligature_subst.ligatures = {} lookup = otTables.Lookup()
lookup.LookupType = 4
lookup = otTables.Lookup() lookup.LookupFlag = 0
lookup.LookupType = 4 lookup.SubTableCount = 1
lookup.LookupFlag = 0 lookup.SubTable = [ligature_subst]
lookup.SubTableCount = 1
lookup.SubTable = [ligature_subst] font['GSUB'] = add_emoji_gsub.create_simple_gsub([lookup])
else:
font['GSUB'] = add_emoji_gsub.create_simple_gsub([lookup]) lookup = font['GSUB'].table.LookupList.Lookup[0]
else: assert lookup.LookupFlag == 0
lookup = font['GSUB'].table.LookupList.Lookup[0]
assert lookup.LookupFlag == 0 # importXML doesn't fully init GSUB structures, so help it out
st = lookup.SubTable[0]
# importXML doesn't fully init GSUB structures, so help it out if not hasattr(lookup, 'LookupType'):
st = lookup.SubTable[0] assert st.LookupType == 4
if not hasattr(lookup, 'LookupType'): setattr(lookup, 'LookupType', 4)
assert st.LookupType == 4
setattr(lookup, 'LookupType', 4) if not hasattr(st, 'ligatures'):
setattr(st, 'ligatures', {})
if not hasattr(st, 'ligatures'):
setattr(st, 'ligatures', {}) return lookup
return lookup
def add_ligature_sequences(font, seqs, aliases):
"""Add ligature sequences."""
def add_ligature_sequences(font, seqs, aliases):
"""Add ligature sequences.""" seq_to_target_name = {
seq: seq_name(seq) for seq in seqs if len(seq) > 1}
seq_to_target_name = { if aliases:
seq: seq_name(seq) for seq in seqs if len(seq) > 1} seq_to_target_name.update({
if aliases: seq: seq_name(aliases[seq]) for seq in aliases if len(seq) > 1})
seq_to_target_name.update({ if not seq_to_target_name:
seq: seq_name(aliases[seq]) for seq in aliases if len(seq) > 1}) return
if not seq_to_target_name:
return rtl_seq_to_target_name = {
get_rtl_seq(seq): name for seq, name in seq_to_target_name.items()}
rtl_seq_to_target_name = { seq_to_target_name.update(rtl_seq_to_target_name)
get_rtl_seq(seq): name for seq, name in seq_to_target_name.iteritems()} # sequences that don't have rtl variants get mapped to the empty sequence,
seq_to_target_name.update(rtl_seq_to_target_name) # delete it.
# sequences that don't have rtl variants get mapped to the empty sequence, if () in seq_to_target_name:
# delete it. del seq_to_target_name[()]
if () in seq_to_target_name:
del seq_to_target_name[()] # organize by first codepoint in sequence
keyed_ligatures = collections.defaultdict(list)
# organize by first codepoint in sequence for t in seq_to_target_name.items():
keyed_ligatures = collections.defaultdict(list) first_cp = t[0][0]
for t in seq_to_target_name.iteritems(): keyed_ligatures[first_cp].append(t)
first_cp = t[0][0]
keyed_ligatures[first_cp].append(t) def add_ligature(lookup, cmap, seq, name):
# The sequences consist of codepoints, but the entries in the ligature table
def add_ligature(lookup, cmap, seq, name): # are glyph names. Aliasing can give single codepoints names based on
# The sequences consist of codepoints, but the entries in the ligature table # sequences (e.g. 'guardsman' with 'male guardsman') so we map the
# are glyph names. Aliasing can give single codepoints names based on # codepoints through the cmap to get the glyph names.
# sequences (e.g. 'guardsman' with 'male guardsman') so we map the glyph_names = [cmap[cp] for cp in seq]
# codepoints through the cmap to get the glyph names.
glyph_names = [cmap[cp] for cp in seq] lig = otTables.Ligature()
lig.CompCount = len(seq)
lig = otTables.Ligature() lig.Component = glyph_names[1:]
lig.CompCount = len(seq) lig.LigGlyph = name
lig.Component = glyph_names[1:]
lig.LigGlyph = name ligatures = lookup.SubTable[0].ligatures
first_name = glyph_names[0]
ligatures = lookup.SubTable[0].ligatures try:
first_name = glyph_names[0] ligatures[first_name].append(lig)
try: except KeyError:
ligatures[first_name].append(lig) ligatures[first_name] = [lig]
except KeyError:
ligatures[first_name] = [lig] lookup = get_gsub_ligature_lookup(font)
cmap = get_font_cmap(font)
lookup = get_gsub_ligature_lookup(font) for first_cp in sorted(keyed_ligatures):
cmap = get_font_cmap(font) pairs = keyed_ligatures[first_cp]
for first_cp in sorted(keyed_ligatures):
pairs = keyed_ligatures[first_cp] # Sort longest first, this ensures longer sequences with common prefixes
# are handled before shorter ones. The secondary sort is a standard
# Sort longest first, this ensures longer sequences with common prefixes # sort on the codepoints in the sequence.
# are handled before shorter ones. The secondary sort is a standard pairs.sort(key = lambda pair: (-len(pair[0]), pair[0]))
# sort on the codepoints in the sequence. for seq, name in pairs:
pairs.sort(key = lambda pair: (-len(pair[0]), pair[0])) add_ligature(lookup, cmap, seq, name)
for seq, name in pairs:
add_ligature(lookup, cmap, seq, name)
def update_font_data(font, seq_to_advance, vadvance, aliases):
"""Update the font's cmap, hmtx, GSUB, and GlyphOrder tables."""
def update_font_data(font, seq_to_advance, vadvance, aliases): seqs = get_all_seqs(font, seq_to_advance)
"""Update the font's cmap, hmtx, GSUB, and GlyphOrder tables.""" add_glyph_data(font, seqs, seq_to_advance, vadvance)
seqs = get_all_seqs(font, seq_to_advance) add_aliases_to_cmap(font, aliases)
add_glyph_data(font, seqs, seq_to_advance, vadvance) add_ligature_sequences(font, seqs, aliases)
add_aliases_to_cmap(font, aliases)
add_ligature_sequences(font, seqs, aliases)
def apply_aliases(seq_dict, aliases):
"""Aliases is a mapping from sequence to replacement sequence. We can use
def apply_aliases(seq_dict, aliases): an alias if the target is a key in the dictionary. Furthermore, if the
"""Aliases is a mapping from sequence to replacement sequence. We can use source is a key in the dictionary, we can delete it. This updates the
an alias if the target is a key in the dictionary. Furthermore, if the dictionary and returns the usable aliases."""
source is a key in the dictionary, we can delete it. This updates the usable_aliases = {}
dictionary and returns the usable aliases.""" for k, v in aliases.items():
usable_aliases = {} if v in seq_dict:
for k, v in aliases.iteritems(): usable_aliases[k] = v
if v in seq_dict: if k in seq_dict:
usable_aliases[k] = v del seq_dict[k]
if k in seq_dict: return usable_aliases
del seq_dict[k]
return usable_aliases
def update_ttx(in_file, out_file, image_dirs, prefix, ext, aliases_file):
if ext != '.png':
def update_ttx(in_file, out_file, image_dirs, prefix, ext, aliases_file): raise Exception('extension "%s" not supported' % ext)
if ext != '.png':
raise Exception('extension "%s" not supported' % ext) seq_to_file = collect_seq_to_file(image_dirs, prefix, ext)
if not seq_to_file:
seq_to_file = collect_seq_to_file(image_dirs, prefix, ext) raise ValueError(
if not seq_to_file: 'no sequences with prefix "%s" and extension "%s" in %s' % (
raise ValueError( prefix, ext, ', '.join(image_dirs)))
'no sequences with prefix "%s" and extension "%s" in %s' % (
prefix, ext, ', '.join(image_dirs))) aliases = None
if aliases_file:
aliases = None aliases = add_aliases.read_emoji_aliases(aliases_file)
if aliases_file: aliases = apply_aliases(seq_to_file, aliases)
aliases = add_aliases.read_emoji_aliases(aliases_file)
aliases = apply_aliases(seq_to_file, aliases) font = ttx.TTFont()
font.importXML(in_file)
font = ttx.TTFont()
font.importXML(in_file) lineheight = font['hhea'].ascent - font['hhea'].descent
map_fn = get_png_file_to_advance_mapper(lineheight)
lineheight = font['hhea'].ascent - font['hhea'].descent seq_to_advance = remap_values(seq_to_file, map_fn)
map_fn = get_png_file_to_advance_mapper(lineheight)
seq_to_advance = remap_values(seq_to_file, map_fn) vadvance = font['vhea'].advanceHeightMax if 'vhea' in font else lineheight
vadvance = font['vhea'].advanceHeightMax if 'vhea' in font else lineheight update_font_data(font, seq_to_advance, vadvance, aliases)
update_font_data(font, seq_to_advance, vadvance, aliases) font.saveXML(out_file)
font.saveXML(out_file)
def main():
parser = argparse.ArgumentParser()
def main(): parser.add_argument(
parser = argparse.ArgumentParser() '-f', '--in_file', help='ttx input file', metavar='file', required=True)
parser.add_argument( parser.add_argument(
'-f', '--in_file', help='ttx input file', metavar='file', required=True) '-o', '--out_file', help='ttx output file', metavar='file', required=True)
parser.add_argument( parser.add_argument(
'-o', '--out_file', help='ttx output file', metavar='file', required=True) '-d', '--image_dirs', help='directories containing image files',
parser.add_argument( nargs='+', metavar='dir', required=True)
'-d', '--image_dirs', help='directories containing image files', parser.add_argument(
nargs='+', metavar='dir', required=True) '-p', '--prefix', help='file prefix (default "emoji_u")',
parser.add_argument( metavar='pfx', default='emoji_u')
'-p', '--prefix', help='file prefix (default "emoji_u")', parser.add_argument(
metavar='pfx', default='emoji_u') '-e', '--ext', help='file extension (default ".png", currently only '
parser.add_argument( '".png" is supported', metavar='ext', default='.png')
'-e', '--ext', help='file extension (default ".png", currently only ' parser.add_argument(
'".png" is supported', metavar='ext', default='.png') '-a', '--aliases', help='process alias table', const='emoji_aliases.txt',
parser.add_argument( nargs='?', metavar='file')
'-a', '--aliases', help='process alias table', const='emoji_aliases.txt', args = parser.parse_args()
nargs='?', metavar='file')
args = parser.parse_args() update_ttx(
args.in_file, args.out_file, args.image_dirs, args.prefix, args.ext,
update_ttx( args.aliases)
args.in_file, args.out_file, args.image_dirs, args.prefix, args.ext,
args.aliases)
if __name__ == '__main__':
main()
if __name__ == '__main__':
main()

View file

@ -1,295 +1,295 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright 2015 Google, Inc. All Rights Reserved. # Copyright 2015 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Doug Felt # Google Author(s): Doug Felt
"""Tool to update GSUB, hmtx, cmap, glyf tables with svg image glyphs.""" """Tool to update GSUB, hmtx, cmap, glyf tables with svg image glyphs."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import glob import glob
import logging import logging
import os import os
import re import re
import sys import sys
from fontTools.ttLib.tables import otTables from fontTools.ttLib.tables import otTables
from fontTools.ttLib.tables import _g_l_y_f from fontTools.ttLib.tables import _g_l_y_f
from fontTools.ttLib.tables import S_V_G_ as SVG from fontTools.ttLib.tables import S_V_G_ as SVG
from fontTools import ttx from fontTools import ttx
from nototools import tool_utils from nototools import tool_utils
import add_emoji_gsub import add_emoji_gsub
import svg_builder import svg_builder
class FontBuilder(object): class FontBuilder(object):
"""A utility for mutating a ttx font. This maintains glyph_order, cmap, and """A utility for mutating a ttx font. This maintains glyph_order, cmap, and
hmtx tables, and optionally GSUB, glyf, and SVN tables as well.""" hmtx tables, and optionally GSUB, glyf, and SVN tables as well."""
def __init__(self, font): def __init__(self, font):
self.font = font; self.font = font;
self.glyph_order = font.getGlyphOrder() self.glyph_order = font.getGlyphOrder()
self.cmap = font['cmap'].tables[0].cmap self.cmap = font['cmap'].tables[0].cmap
self.hmtx = font['hmtx'].metrics self.hmtx = font['hmtx'].metrics
def init_gsub(self): def init_gsub(self):
"""Call this if you are going to add ligatures to the font. Creates a GSUB """Call this if you are going to add ligatures to the font. Creates a GSUB
table if there isn't one already.""" table if there isn't one already."""
if hasattr(self, 'ligatures'): if hasattr(self, 'ligatures'):
return return
font = self.font font = self.font
if 'GSUB' not in font: if 'GSUB' not in font:
ligature_subst = otTables.LigatureSubst() ligature_subst = otTables.LigatureSubst()
ligature_subst.ligatures = {} ligature_subst.ligatures = {}
lookup = otTables.Lookup() lookup = otTables.Lookup()
lookup.LookupType = 4 lookup.LookupType = 4
lookup.LookupFlag = 0 lookup.LookupFlag = 0
lookup.SubTableCount = 1 lookup.SubTableCount = 1
lookup.SubTable = [ligature_subst] lookup.SubTable = [ligature_subst]
font['GSUB'] = add_emoji_gsub.create_simple_gsub([lookup]) font['GSUB'] = add_emoji_gsub.create_simple_gsub([lookup])
else: else:
lookup = font['GSUB'].table.LookupList.Lookup[0] lookup = font['GSUB'].table.LookupList.Lookup[0]
assert lookup.LookupType == 4 assert lookup.LookupType == 4
assert lookup.LookupFlag == 0 assert lookup.LookupFlag == 0
self.ligatures = lookup.SubTable[0].ligatures self.ligatures = lookup.SubTable[0].ligatures
def init_glyf(self): def init_glyf(self):
"""Call this if you need to create empty glyf entries in the font when you """Call this if you need to create empty glyf entries in the font when you
add a new glyph.""" add a new glyph."""
if hasattr(self, 'glyphs'): if hasattr(self, 'glyphs'):
return return
font = self.font font = self.font
if 'glyf' not in font: if 'glyf' not in font:
glyf_table = _g_l_y_f.table__g_l_y_f() glyf_table = _g_l_y_f.table__g_l_y_f()
glyf_table.glyphs = {} glyf_table.glyphs = {}
glyf_table.glyphOrder = self.glyph_order glyf_table.glyphOrder = self.glyph_order
font['glyf'] = glyf_table font['glyf'] = glyf_table
self.glyphs = font['glyf'].glyphs self.glyphs = font['glyf'].glyphs
def init_svg(self): def init_svg(self):
"""Call this if you expect to add SVG images in the font. This calls """Call this if you expect to add SVG images in the font. This calls
init_glyf since SVG support currently requires fallback glyf records for init_glyf since SVG support currently requires fallback glyf records for
each SVG image.""" each SVG image."""
if hasattr(self, 'svgs'): if hasattr(self, 'svgs'):
return return
# svg requires glyf # svg requires glyf
self.init_glyf() self.init_glyf()
font = self.font font = self.font
if 'SVG ' not in font: if 'SVG ' not in font:
svg_table = SVG.table_S_V_G_() svg_table = SVG.table_S_V_G_()
svg_table.docList = [] svg_table.docList = []
svg_table.colorPalettes = None svg_table.colorPalettes = None
font['SVG '] = svg_table font['SVG '] = svg_table
self.svgs = font['SVG '].docList self.svgs = font['SVG '].docList
def glyph_name(self, string): def glyph_name(self, string):
return "_".join(["u%04X" % ord(char) for char in string]) return "_".join(["u%04X" % ord(char) for char in string])
def glyph_name_to_index(self, name): def glyph_name_to_index(self, name):
return self.glyph_order.index(name) if name in self.glyph_order else -1; return self.glyph_order.index(name) if name in self.glyph_order else -1;
def glyph_index_to_name(self, glyph_index): def glyph_index_to_name(self, glyph_index):
if glyph_index < len(self.glyph_order): if glyph_index < len(self.glyph_order):
return self.glyph_order[glyph_index] return self.glyph_order[glyph_index]
return '' return ''
def have_glyph(self, name): def have_glyph(self, name):
return self.name_to_glyph_index >= 0 return self.name_to_glyph_index >= 0
def _add_ligature(self, glyphstr): def _add_ligature(self, glyphstr):
lig = otTables.Ligature() lig = otTables.Ligature()
lig.CompCount = len(glyphstr) lig.CompCount = len(glyphstr)
lig.Component = [self.glyph_name(ch) for ch in glyphstr[1:]] lig.Component = [self.glyph_name(ch) for ch in glyphstr[1:]]
lig.LigGlyph = self.glyph_name(glyphstr) lig.LigGlyph = self.glyph_name(glyphstr)
first = self.glyph_name(glyphstr[0]) first = self.glyph_name(glyphstr[0])
try: try:
self.ligatures[first].append(lig) self.ligatures[first].append(lig)
except KeyError: except KeyError:
self.ligatures[first] = [lig] self.ligatures[first] = [lig]
def _add_empty_glyph(self, glyphstr, name): def _add_empty_glyph(self, glyphstr, name):
"""Create an empty glyph. If glyphstr is not a ligature, add a cmap entry """Create an empty glyph. If glyphstr is not a ligature, add a cmap entry
for it.""" for it."""
if len(glyphstr) == 1: if len(glyphstr) == 1:
self.cmap[ord(glyphstr)] = name self.cmap[ord(glyphstr)] = name
self.hmtx[name] = [0, 0] self.hmtx[name] = [0, 0]
self.glyph_order.append(name) self.glyph_order.append(name)
if hasattr(self, 'glyphs'): if hasattr(self, 'glyphs'):
self.glyphs[name] = _g_l_y_f.Glyph() self.glyphs[name] = _g_l_y_f.Glyph()
def add_components_and_ligature(self, glyphstr): def add_components_and_ligature(self, glyphstr):
"""Convert glyphstr to a name and check if it already exists. If not, check """Convert glyphstr to a name and check if it already exists. If not, check
if it is a ligature (longer than one codepoint), and if it is, generate if it is a ligature (longer than one codepoint), and if it is, generate
empty glyphs with cmap entries for any missing ligature components and add a empty glyphs with cmap entries for any missing ligature components and add a
ligature record. Then generate an empty glyph for the name. Return a tuple ligature record. Then generate an empty glyph for the name. Return a tuple
with the name, index, and a bool indicating whether the glyph already with the name, index, and a bool indicating whether the glyph already
existed.""" existed."""
name = self.glyph_name(glyphstr) name = self.glyph_name(glyphstr)
index = self.glyph_name_to_index(name) index = self.glyph_name_to_index(name)
exists = index >= 0 exists = index >= 0
if not exists: if not exists:
if len(glyphstr) > 1: if len(glyphstr) > 1:
for char in glyphstr: for char in glyphstr:
if ord(char) not in self.cmap: if ord(char) not in self.cmap:
char_name = self.glyph_name(char) char_name = self.glyph_name(char)
self._add_empty_glyph(char, char_name) self._add_empty_glyph(char, char_name)
self._add_ligature(glyphstr) self._add_ligature(glyphstr)
index = len(self.glyph_order) index = len(self.glyph_order)
self._add_empty_glyph(glyphstr, name) self._add_empty_glyph(glyphstr, name)
return name, index, exists return name, index, exists
def add_svg(self, doc, hmetrics, name, index): def add_svg(self, doc, hmetrics, name, index):
"""Add an svg table entry. If hmetrics is not None, update the hmtx table. """Add an svg table entry. If hmetrics is not None, update the hmtx table.
This expects the glyph has already been added.""" This expects the glyph has already been added."""
# sanity check to make sure name and index correspond. # sanity check to make sure name and index correspond.
assert name == self.glyph_index_to_name(index) assert name == self.glyph_index_to_name(index)
if hmetrics: if hmetrics:
self.hmtx[name] = hmetrics self.hmtx[name] = hmetrics
svg_record = (doc, index, index) # startGlyphId, endGlyphId are the same svg_record = (doc, index, index) # startGlyphId, endGlyphId are the same
self.svgs.append(svg_record) self.svgs.append(svg_record)
def collect_glyphstr_file_pairs(prefix, ext, include=None, exclude=None, verbosity=1): def collect_glyphstr_file_pairs(prefix, ext, include=None, exclude=None, verbosity=1):
"""Scan files with the given prefix and extension, and return a list of """Scan files with the given prefix and extension, and return a list of
(glyphstr, filename) where glyphstr is the character or ligature, and filename (glyphstr, filename) where glyphstr is the character or ligature, and filename
is the image file associated with it. The glyphstr is formed by decoding the is the image file associated with it. The glyphstr is formed by decoding the
filename (exclusive of the prefix) as a sequence of hex codepoints separated filename (exclusive of the prefix) as a sequence of hex codepoints separated
by underscore. Include, if defined, is a regex string to include only matched by underscore. Include, if defined, is a regex string to include only matched
filenames. Exclude, if defined, is a regex string to exclude matched filenames. Exclude, if defined, is a regex string to exclude matched
filenames, and is applied after include.""" filenames, and is applied after include."""
image_files = {} image_files = {}
glob_pat = "%s*.%s" % (prefix, ext) glob_pat = "%s*.%s" % (prefix, ext)
leading = len(prefix) leading = len(prefix)
trailing = len(ext) + 1 # include dot trailing = len(ext) + 1 # include dot
logging.info("Looking for images matching '%s'.", glob_pat) logging.info("Looking for images matching '%s'.", glob_pat)
ex_count = 0 ex_count = 0
ex = re.compile(exclude) if exclude else None ex = re.compile(exclude) if exclude else None
inc = re.compile(include) if include else None inc = re.compile(include) if include else None
if inc: if inc:
logging.info("Including images matching '%s'.", include) logging.info("Including images matching '%s'.", include)
if ex: if ex:
logging.info("Excluding images matching '%s'.", exclude) logging.info("Excluding images matching '%s'.", exclude)
for image_file in glob.glob(glob_pat): for image_file in glob.glob(glob_pat):
if inc and not inc.search(image_file): if inc and not inc.search(image_file):
continue continue
if ex and ex.search(image_file): if ex and ex.search(image_file):
if verbosity > 1: if verbosity > 1:
print("Exclude %s" % image_file) print("Exclude %s" % image_file)
ex_count += 1 ex_count += 1
continue continue
codes = image_file[leading:-trailing] codes = image_file[leading:-trailing]
if "_" in codes: if "_" in codes:
pieces = codes.split ("_") pieces = codes.split ("_")
u = "".join ([unichr(int(code, 16)) for code in pieces]) u = "".join ([unichr(int(code, 16)) for code in pieces])
else: else:
u = unichr(int(codes, 16)) u = unichr(int(codes, 16))
image_files[u] = image_file image_files[u] = image_file
if ex_count: if ex_count:
logging.info("Excluded %d files.", ex_count) logging.info("Excluded %d files.", ex_count)
if not image_files: if not image_files:
raise Exception ("No image files matching '%s'.", glob_pat) raise Exception ("No image files matching '%s'.", glob_pat)
logging.info("Matched %s files.", len(image_files)) logging.info("Matched %s files.", len(image_files))
return image_files.items() return image_files.items()
def sort_glyphstr_tuples(glyphstr_tuples): def sort_glyphstr_tuples(glyphstr_tuples):
"""The list contains tuples whose first element is a string representing a """The list contains tuples whose first element is a string representing a
character or ligature. It is sorted with shorter glyphstrs first, then character or ligature. It is sorted with shorter glyphstrs first, then
alphabetically. This ensures that ligature components are added to the font alphabetically. This ensures that ligature components are added to the font
before any ligatures that contain them.""" before any ligatures that contain them."""
glyphstr_tuples.sort(key=lambda t: (len(t[0]), t[0])) glyphstr_tuples.sort(key=lambda t: (len(t[0]), t[0]))
def add_image_glyphs(in_file, out_file, pairs): def add_image_glyphs(in_file, out_file, pairs):
"""Add images from pairs (glyphstr, filename) to .ttx file in_file and write """Add images from pairs (glyphstr, filename) to .ttx file in_file and write
to .ttx file out_file.""" to .ttx file out_file."""
font = ttx.TTFont() font = ttx.TTFont()
font.importXML(in_file) font.importXML(in_file)
sort_glyphstr_tuples(pairs) sort_glyphstr_tuples(pairs)
font_builder = FontBuilder(font) font_builder = FontBuilder(font)
# we've already sorted by length, so the longest glyphstrs are at the end. To # we've already sorted by length, so the longest glyphstrs are at the end. To
# see if we have ligatures, we just need to check the last one. # see if we have ligatures, we just need to check the last one.
if len(pairs[-1][0]) > 1: if len(pairs[-1][0]) > 1:
font_builder.init_gsub() font_builder.init_gsub()
img_builder = svg_builder.SvgBuilder(font_builder) img_builder = svg_builder.SvgBuilder(font_builder)
for glyphstr, filename in pairs: for glyphstr, filename in pairs:
logging.debug("Adding glyph for U+%s", ",".join( logging.debug("Adding glyph for U+%s", ",".join(
["%04X" % ord(char) for char in glyphstr])) ["%04X" % ord(char) for char in glyphstr]))
img_builder.add_from_filename(glyphstr, filename) img_builder.add_from_filename(glyphstr, filename)
font.saveXML(out_file) font.saveXML(out_file)
logging.info("Added %s images to %s", len(pairs), out_file) logging.info("Added %s images to %s", len(pairs), out_file)
def main(argv): def main(argv):
usage = """This will search for files that have image_prefix followed by one usage = """This will search for files that have image_prefix followed by one
or more hex numbers (separated by underscore if more than one), and end in or more hex numbers (separated by underscore if more than one), and end in
".svg". For example, if image_prefix is "icons/u", then files with names like ".svg". For example, if image_prefix is "icons/u", then files with names like
"icons/u1F4A9.svg" or "icons/u1F1EF_1F1F5.svg" will be loaded. The script "icons/u1F4A9.svg" or "icons/u1F1EF_1F1F5.svg" will be loaded. The script
then adds cmap, htmx, and potentially GSUB entries for the Unicode characters then adds cmap, htmx, and potentially GSUB entries for the Unicode characters
found. The advance width will be chosen based on image aspect ratio. If found. The advance width will be chosen based on image aspect ratio. If
Unicode values outside the BMP are desired, the existing cmap table should be Unicode values outside the BMP are desired, the existing cmap table should be
of the appropriate (format 12) type. Only the first cmap table and the first of the appropriate (format 12) type. Only the first cmap table and the first
GSUB lookup (if existing) are modified.""" GSUB lookup (if existing) are modified."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Update cmap, glyf, GSUB, and hmtx tables from image glyphs.', description='Update cmap, glyf, GSUB, and hmtx tables from image glyphs.',
epilog=usage) epilog=usage)
parser.add_argument( parser.add_argument(
'in_file', help='Input ttx file name.', metavar='fname') 'in_file', help='Input ttx file name.', metavar='fname')
parser.add_argument( parser.add_argument(
'out_file', help='Output ttx file name.', metavar='fname') 'out_file', help='Output ttx file name.', metavar='fname')
parser.add_argument( parser.add_argument(
'image_prefix', help='Location and prefix of image files.', 'image_prefix', help='Location and prefix of image files.',
metavar='path') metavar='path')
parser.add_argument( parser.add_argument(
'-i', '--include', help='include files whoses name matches this regex', '-i', '--include', help='include files whoses name matches this regex',
metavar='regex') metavar='regex')
parser.add_argument( parser.add_argument(
'-e', '--exclude', help='exclude files whose name matches this regex', '-e', '--exclude', help='exclude files whose name matches this regex',
metavar='regex') metavar='regex')
parser.add_argument( parser.add_argument(
'-l', '--loglevel', help='log level name', default='warning') '-l', '--loglevel', help='log level name', default='warning')
args = parser.parse_args(argv) args = parser.parse_args(argv)
tool_utils.setup_logging(args.loglevel) tool_utils.setup_logging(args.loglevel)
pairs = collect_glyphstr_file_pairs( pairs = collect_glyphstr_file_pairs(
args.image_prefix, 'svg', include=args.include, exclude=args.exclude) args.image_prefix, 'svg', include=args.include, exclude=args.exclude)
add_image_glyphs(args.in_file, args.out_file, pairs) add_image_glyphs(args.in_file, args.out_file, pairs)
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv[1:]) main(sys.argv[1:])

View file

@ -1,406 +1,463 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2016 Google Inc. All rights reserved. # Copyright 2016 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Compare emoji image file namings against unicode property data.""" """Compare emoji image file namings against unicode property data."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import collections import collections
import glob import glob
import os import os
from os import path from os import path
import re import re
import sys import sys
from nototools import unicode_data from nototools import unicode_data
import add_aliases
DATA_ROOT = path.dirname(path.abspath(__file__))
ZWJ = 0x200d
ZWJ = 0x200d EMOJI_VS = 0xfe0f
EMOJI_VS = 0xfe0f
END_TAG = 0xe007f
def _is_regional_indicator(cp):
return 0x1f1e6 <= cp <= 0x1f1ff def _make_tag_set():
tag_set = set()
tag_set |= set(range(0xe0030, 0xe003a)) # 0-9
def _is_skintone_modifier(cp): tag_set |= set(range(0xe0061, 0xe007b)) # a-z
return 0x1f3fb <= cp <= 0x1f3ff tag_set.add(END_TAG)
return tag_set
def _seq_string(seq): TAG_SET = _make_tag_set()
return '_'.join('%04x' % cp for cp in seq)
_namedata = None
def strip_vs(seq):
return tuple(cp for cp in seq if cp != EMOJI_VS) def seq_name(seq):
global _namedata
_namedata = None
if not _namedata:
def seq_name(seq): def strip_vs_map(seq_map):
global _namedata return {
unicode_data.strip_emoji_vs(k): v
if not _namedata: for k, v in seq_map.iteritems()}
def strip_vs_map(seq_map): _namedata = [
return { strip_vs_map(unicode_data.get_emoji_combining_sequences()),
strip_vs(k): v strip_vs_map(unicode_data.get_emoji_flag_sequences()),
for k, v in seq_map.iteritems()} strip_vs_map(unicode_data.get_emoji_modifier_sequences()),
_namedata = [ strip_vs_map(unicode_data.get_emoji_zwj_sequences()),
strip_vs_map(unicode_data.get_emoji_combining_sequences()), ]
strip_vs_map(unicode_data.get_emoji_flag_sequences()),
strip_vs_map(unicode_data.get_emoji_modifier_sequences()), if len(seq) == 1:
strip_vs_map(unicode_data.get_emoji_zwj_sequences()), return unicode_data.name(seq[0], None)
]
for data in _namedata:
if len(seq) == 1: if seq in data:
return unicode_data.name(seq[0], None) return data[seq]
if EMOJI_VS in seq:
for data in _namedata: non_vs_seq = unicode_data.strip_emoji_vs(seq)
if seq in data: for data in _namedata:
return data[seq] if non_vs_seq in data:
if EMOJI_VS in seq: return data[non_vs_seq]
non_vs_seq = strip_vs(seq)
for data in _namedata: return None
if non_vs_seq in data:
return data[non_vs_seq]
def _check_no_vs(sorted_seq_to_filepath):
return None """Our image data does not use emoji presentation variation selectors."""
for seq, fp in sorted_seq_to_filepath.iteritems():
if EMOJI_VS in seq:
def _check_valid_emoji(sorted_seq_to_filepath): print('check no VS: FE0F in path: %s' % fp)
"""Ensure all emoji are either valid emoji or specific chars."""
valid_cps = set(unicode_data.get_emoji() | unicode_data.proposed_emoji_cps()) def _check_valid_emoji_cps(sorted_seq_to_filepath, unicode_version):
valid_cps.add(0x200d) # ZWJ """Ensure all cps in these sequences are valid emoji cps or specific cps
valid_cps.add(0x20e3) # combining enclosing keycap used in forming emoji sequences. This is a 'pre-check' that reports
valid_cps.add(0xfe0f) # variation selector (emoji presentation) this specific problem."""
valid_cps.add(0xfe82b) # PUA value for unknown flag
valid_cps = set(unicode_data.get_emoji())
not_emoji = {} if unicode_version is None or unicode_version >= unicode_data.PROPOSED_EMOJI_AGE:
for seq, fp in sorted_seq_to_filepath.iteritems(): valid_cps |= unicode_data.proposed_emoji_cps()
for cp in seq: else:
if cp not in valid_cps: valid_cps = set(
if cp not in not_emoji: cp for cp in valid_cps if unicode_data.age(cp) <= unicode_version)
not_emoji[cp] = [] valid_cps.add(0x200d) # ZWJ
not_emoji[cp].append(fp) valid_cps.add(0x20e3) # combining enclosing keycap
valid_cps.add(0xfe0f) # variation selector (emoji presentation)
if len(not_emoji): valid_cps.add(0xfe82b) # PUA value for unknown flag
print('%d non-emoji found:' % len(not_emoji), file=sys.stderr) valid_cps |= TAG_SET # used in subregion tag sequences
for cp in sorted(not_emoji):
print('%04x (in %s)' % (cp, ', '.join(not_emoji[cp])), file=sys.stderr) not_emoji = {}
for seq, fp in sorted_seq_to_filepath.iteritems():
for cp in seq:
def _check_zwj(sorted_seq_to_filepath): if cp not in valid_cps:
"""Ensure zwj is only between two appropriate emoji.""" if cp not in not_emoji:
ZWJ = 0x200D not_emoji[cp] = []
EMOJI_PRESENTATION_VS = 0xFE0F not_emoji[cp].append(fp)
for seq, fp in sorted_seq_to_filepath.iteritems(): if len(not_emoji):
if ZWJ not in seq: print(
continue 'check valid emoji cps: %d non-emoji cp found' % len(not_emoji),
if seq[0] == 0x200d: file=sys.stderr)
print('zwj at head of sequence in %s' % fp, file=sys.stderr) for cp in sorted(not_emoji):
if len(seq) == 1: fps = not_emoji[cp]
continue print(
if seq[-1] == 0x200d: 'check valid emoji cps: %04x (in %d sequences)' % (cp, len(fps)),
print('zwj at end of sequence in %s' % fp, file=sys.stderr) file=sys.stderr)
for i, cp in enumerate(seq):
if cp == ZWJ:
if i > 0: def _check_zwj(sorted_seq_to_filepath):
pcp = seq[i-1] """Ensure zwj is only between two appropriate emoji. This is a 'pre-check'
if pcp != EMOJI_PRESENTATION_VS and not unicode_data.is_emoji(pcp): that reports this specific problem."""
print('non-emoji %04x preceeds ZWJ in %s' % (pcp, fp), file=sys.stderr)
if i < len(seq) - 1: for seq, fp in sorted_seq_to_filepath.iteritems():
fcp = seq[i+1] if ZWJ not in seq:
if not unicode_data.is_emoji(fcp): continue
print('non-emoji %04x follows ZWJ in %s' % (fcp, fp), file=sys.stderr) if seq[0] == ZWJ:
print('check zwj: zwj at head of sequence in %s' % fp, file=sys.stderr)
if len(seq) == 1:
def _check_flags(sorted_seq_to_filepath): continue
"""Ensure regional indicators are only in sequences of one or two, and if seq[-1] == ZWJ:
never mixed.""" print('check zwj: zwj at end of sequence in %s' % fp, file=sys.stderr)
for seq, fp in sorted_seq_to_filepath.iteritems(): for i, cp in enumerate(seq):
have_reg = None if cp == ZWJ:
for cp in seq: if i > 0:
is_reg = _is_regional_indicator(cp) pcp = seq[i-1]
if have_reg == None: if pcp != EMOJI_VS and not unicode_data.is_emoji(pcp):
have_reg = is_reg print(
elif have_reg != is_reg: 'check zwj: non-emoji %04x preceeds ZWJ in %s' % (pcp, fp),
print('mix of regional and non-regional in %s' % fp, file=sys.stderr) file=sys.stderr)
if have_reg and len(seq) > 2: if i < len(seq) - 1:
# We provide dummy glyphs for regional indicators, so there are sequences fcp = seq[i+1]
# with single regional indicator symbols. if not unicode_data.is_emoji(fcp):
print('regional indicator sequence length != 2 in %s' % fp, file=sys.stderr) print(
'check zwj: non-emoji %04x follows ZWJ in %s' % (fcp, fp),
file=sys.stderr)
def _check_skintone(sorted_seq_to_filepath):
"""Ensure skin tone modifiers are not applied to emoji that are not defined
to take them. May appear standalone, though. Also check that emoji that take def _check_flags(sorted_seq_to_filepath):
skin tone modifiers have a complete set.""" """Ensure regional indicators are only in sequences of one or two, and
base_to_modifiers = collections.defaultdict(set) never mixed."""
for seq, fp in sorted_seq_to_filepath.iteritems(): for seq, fp in sorted_seq_to_filepath.iteritems():
for i, cp in enumerate(seq): have_reg = None
if _is_skintone_modifier(cp): for cp in seq:
if i == 0: is_reg = unicode_data.is_regional_indicator(cp)
if len(seq) > 1: if have_reg == None:
print('skin color selector first in sequence %s' % fp, file=sys.stderr) have_reg = is_reg
# standalone are ok elif have_reg != is_reg:
continue print(
pcp = seq[i-1] 'check flags: mix of regional and non-regional in %s' % fp,
if not unicode_data.is_emoji_modifier_base(pcp): file=sys.stderr)
print(( if have_reg and len(seq) > 2:
'emoji skintone modifier applied to non-base at %d: %s' % (i, fp)), file=sys.stderr) # We provide dummy glyphs for regional indicators, so there are sequences
elif unicode_data.is_emoji_modifier_base(cp): # with single regional indicator symbols, the len check handles this.
if i < len(seq) - 1 and _is_skintone_modifier(seq[i+1]): print(
base_to_modifiers[cp].add(seq[i+1]) 'check flags: regional indicator sequence length != 2 in %s' % fp,
elif cp not in base_to_modifiers: file=sys.stderr)
base_to_modifiers[cp] = set()
for cp, modifiers in sorted(base_to_modifiers.iteritems()): def _check_tags(sorted_seq_to_filepath):
if len(modifiers) != 5: """Ensure tag sequences (for subregion flags) conform to the spec. We don't
print('emoji base %04x has %d modifiers defined (%s) in %s' % ( validate against CLDR, just that there's a sequence of 2 or more tags starting
cp, len(modifiers), and ending with the appropriate codepoints."""
', '.join('%04x' % cp for cp in sorted(modifiers)), fp), file=sys.stderr)
BLACK_FLAG = 0x1f3f4
BLACK_FLAG_SET = set([BLACK_FLAG])
def _check_zwj_sequences(seq_to_filepath): for seq, fp in sorted_seq_to_filepath.iteritems():
"""Verify that zwj sequences are valid.""" seq_set = set(cp for cp in seq)
zwj_sequence_to_name = unicode_data.get_emoji_zwj_sequences() overlap_set = seq_set & TAG_SET
# strip emoji variant selectors and add extra mappings if not overlap_set:
zwj_sequence_without_vs_to_name_canonical = {} continue
for seq, seq_name in zwj_sequence_to_name.iteritems(): if seq[0] != BLACK_FLAG:
if EMOJI_VS in seq: print('check tags: bad start tag in %s' % fp)
stripped_seq = strip_vs(seq) elif seq[-1] != END_TAG:
zwj_sequence_without_vs_to_name_canonical[stripped_seq] = (seq_name, seq) print('check tags: bad end tag in %s' % fp)
elif len(seq) < 4:
zwj_seq_to_filepath = { print('check tags: sequence too short in %s' % fp)
seq: fp for seq, fp in seq_to_filepath.iteritems() elif seq_set - TAG_SET != BLACK_FLAG_SET:
if ZWJ in seq} print('check tags: non-tag items in %s' % fp)
for seq, fp in zwj_seq_to_filepath.iteritems():
if seq not in zwj_sequence_to_name: def _check_skintone(sorted_seq_to_filepath):
if seq not in zwj_sequence_without_vs_to_name_canonical: """Ensure skin tone modifiers are not applied to emoji that are not defined
print('zwj sequence not defined: %s' % fp, file=sys.stderr) to take them. May appear standalone, though. Also check that emoji that take
else: skin tone modifiers have a complete set."""
_, can = zwj_sequence_without_vs_to_name_canonical[seq] base_to_modifiers = collections.defaultdict(set)
# print >> sys.stderr, 'canonical sequence %s contains vs: %s' % ( for seq, fp in sorted_seq_to_filepath.iteritems():
# _seq_string(can), fp) for i, cp in enumerate(seq):
if unicode_data.is_skintone_modifier(cp):
def read_emoji_aliases(): if i == 0:
result = {} if len(seq) > 1:
print(
with open(path.join(DATA_ROOT, 'emoji_aliases.txt'), 'r') as f: 'check skintone: skin color selector first in sequence %s' % fp,
for line in f: file=sys.stderr)
ix = line.find('#') # standalone are ok
if (ix > -1): continue
line = line[:ix] pcp = seq[i-1]
line = line.strip() if not unicode_data.is_emoji_modifier_base(pcp):
if not line: print(
continue 'check skintone: emoji skintone modifier applied to non-base ' +
als, trg = (s.strip() for s in line.split(';')) 'at %d: %s' % (i, fp), file=sys.stderr)
als_seq = tuple([int(x, 16) for x in als.split('_')]) else:
try: if pcp not in base_to_modifiers:
trg_seq = tuple([int(x, 16) for x in trg.split('_')]) base_to_modifiers[pcp] = set()
except: base_to_modifiers[pcp].add(cp)
print('cannot process alias %s -> %s' % (als, trg))
continue for cp, modifiers in sorted(base_to_modifiers.iteritems()):
result[als_seq] = trg_seq if len(modifiers) != 5:
return result print(
'check skintone: base %04x has %d modifiers defined (%s) in %s' % (
cp, len(modifiers),
def _check_coverage(seq_to_filepath): ', '.join('%04x' % cp for cp in sorted(modifiers)), fp),
age = 9.0 file=sys.stderr)
non_vs_to_canonical = {}
for k in seq_to_filepath: def _check_zwj_sequences(sorted_seq_to_filepath, unicode_version):
if EMOJI_VS in k: """Verify that zwj sequences are valid for the given unicode version."""
non_vs = strip_vs(k) for seq, fp in sorted_seq_to_filepath.iteritems():
non_vs_to_canonical[non_vs] = k if ZWJ not in seq:
continue
aliases = read_emoji_aliases() age = unicode_data.get_emoji_sequence_age(seq)
for k, v in sorted(aliases.items()): if age is None or unicode_version is not None and age > unicode_version:
if v not in seq_to_filepath and v not in non_vs_to_canonical: print('check zwj sequences: undefined sequence %s' % fp)
print('alias %s missing target %s' % (_seq_string(k), _seq_string(v)))
continue
if k in seq_to_filepath or k in non_vs_to_canonical: def _check_no_alias_sources(sorted_seq_to_filepath):
print('alias %s already exists as %s (%s)' % ( """Check that we don't have sequences that we expect to be aliased to
_seq_string(k), _seq_string(v), seq_name(v))) some other sequence."""
continue aliases = add_aliases.read_default_emoji_aliases()
filename = seq_to_filepath.get(v) or seq_to_filepath[non_vs_to_canonical[v]] for seq, fp in sorted_seq_to_filepath.iteritems():
seq_to_filepath[k] = 'alias:' + filename if seq in aliases:
print('check no alias sources: aliased sequence %s' % fp)
# check single emoji, this includes most of the special chars
emoji = sorted(unicode_data.get_emoji(age=age))
for cp in emoji: def _check_coverage(seq_to_filepath, unicode_version):
if tuple([cp]) not in seq_to_filepath: """Ensure we have all and only the cps and sequences that we need for the
print('missing single %04x (%s)' % (cp, unicode_data.name(cp, '<no name>'))) font as of this version."""
# special characters age = unicode_version
# all but combining enclosing keycap are currently marked as emoji
for cp in [ord('*'), ord('#'), ord(u'\u20e3')] + range(0x30, 0x3a): non_vs_to_canonical = {}
if cp not in emoji and tuple([cp]) not in seq_to_filepath: for k in seq_to_filepath:
print('missing special %04x (%s)' % (cp, unicode_data.name(cp))) if EMOJI_VS in k:
non_vs = unicode_data.strip_emoji_vs(k)
# combining sequences non_vs_to_canonical[non_vs] = k
comb_seq_to_name = sorted(
unicode_data.get_emoji_combining_sequences(age=age).iteritems()) aliases = add_aliases.read_default_emoji_aliases()
for seq, name in comb_seq_to_name: for k, v in sorted(aliases.items()):
if seq not in seq_to_filepath: if v not in seq_to_filepath and v not in non_vs_to_canonical:
# strip vs and try again alias_str = unicode_data.seq_to_string(k)
non_vs_seq = strip_vs(seq) target_str = unicode_data.seq_to_string(v)
if non_vs_seq not in seq_to_filepath: print('coverage: alias %s missing target %s' % (alias_str, target_str))
print('missing combining sequence %s (%s)' % (_seq_string(seq), name)) continue
if k in seq_to_filepath or k in non_vs_to_canonical:
# flag sequences alias_str = unicode_data.seq_to_string(k)
flag_seq_to_name = sorted( target_str = unicode_data.seq_to_string(v)
unicode_data.get_emoji_flag_sequences(age=age).iteritems()) print('coverage: alias %s already exists as %s (%s)' % (
for seq, name in flag_seq_to_name: alias_str, target_str, seq_name(v)))
if seq not in seq_to_filepath: continue
print('missing flag sequence %s (%s)' % (_seq_string(seq), name)) filename = seq_to_filepath.get(v) or seq_to_filepath[non_vs_to_canonical[v]]
seq_to_filepath[k] = 'alias:' + filename
# skin tone modifier sequences
mod_seq_to_name = sorted( # check single emoji, this includes most of the special chars
unicode_data.get_emoji_modifier_sequences(age=age).iteritems()) emoji = sorted(unicode_data.get_emoji(age=age))
for seq, name in mod_seq_to_name: for cp in emoji:
if seq not in seq_to_filepath: if tuple([cp]) not in seq_to_filepath:
print('missing modifier sequence %s (%s)' % ( print(
_seq_string(seq), name)) 'coverage: missing single %04x (%s)' % (
cp, unicode_data.name(cp, '<no name>')))
# zwj sequences
# some of ours include the emoji presentation variation selector and some # special characters
# don't, and the same is true for the canonical sequences. normalize all # all but combining enclosing keycap are currently marked as emoji
# of them to omit it to test coverage, but report the canonical sequence. for cp in [ord('*'), ord('#'), ord(u'\u20e3')] + range(0x30, 0x3a):
zwj_seq_without_vs = set() if cp not in emoji and tuple([cp]) not in seq_to_filepath:
for seq in seq_to_filepath: print('coverage: missing special %04x (%s)' % (cp, unicode_data.name(cp)))
if ZWJ not in seq:
continue # combining sequences
if EMOJI_VS in seq: comb_seq_to_name = sorted(
seq = tuple(cp for cp in seq if cp != EMOJI_VS) unicode_data.get_emoji_combining_sequences(age=age).iteritems())
zwj_seq_without_vs.add(seq) for seq, name in comb_seq_to_name:
if seq not in seq_to_filepath:
for seq, name in sorted( # strip vs and try again
unicode_data.get_emoji_zwj_sequences(age=age).iteritems()): non_vs_seq = unicode_data.strip_emoji_vs(seq)
if EMOJI_VS in seq: if non_vs_seq not in seq_to_filepath:
test_seq = tuple(s for s in seq if s != EMOJI_VS) print('coverage: missing combining sequence %s (%s)' %
else: (unicode_data.seq_to_string(seq), name))
test_seq = seq
if test_seq not in zwj_seq_without_vs: # flag sequences
print('missing (canonical) zwj sequence %s (%s)' % ( flag_seq_to_name = sorted(
_seq_string(seq), name)) unicode_data.get_emoji_flag_sequences(age=age).iteritems())
for seq, name in flag_seq_to_name:
# check for 'unknown flag' if seq not in seq_to_filepath:
# this is either emoji_ufe82b or 'unknown_flag', we filter out things that print('coverage: missing flag sequence %s (%s)' %
# don't start with our prefix so 'unknown_flag' would be excluded by default. (unicode_data.seq_to_string(seq), name))
if tuple([0xfe82b]) not in seq_to_filepath:
print('missing unknown flag PUA fe82b') # skin tone modifier sequences
mod_seq_to_name = sorted(
unicode_data.get_emoji_modifier_sequences(age=age).iteritems())
def check_sequence_to_filepath(seq_to_filepath): for seq, name in mod_seq_to_name:
sorted_seq_to_filepath = collections.OrderedDict( if seq not in seq_to_filepath:
sorted(seq_to_filepath.items())) print('coverage: missing modifier sequence %s (%s)' % (
_check_valid_emoji(sorted_seq_to_filepath) unicode_data.seq_to_string(seq), name))
_check_zwj(sorted_seq_to_filepath)
_check_flags(sorted_seq_to_filepath) # zwj sequences
_check_skintone(sorted_seq_to_filepath) # some of ours include the emoji presentation variation selector and some
_check_zwj_sequences(sorted_seq_to_filepath) # don't, and the same is true for the canonical sequences. normalize all
_check_coverage(sorted_seq_to_filepath) # of them to omit it to test coverage, but report the canonical sequence.
zwj_seq_without_vs = set()
def create_sequence_to_filepath(name_to_dirpath, prefix, suffix): for seq in seq_to_filepath:
"""Check names, and convert name to sequences for names that are ok, if ZWJ not in seq:
returning a sequence to file path mapping. Reports bad segments continue
of a name to stderr.""" if EMOJI_VS in seq:
segment_re = re.compile(r'^[0-9a-f]{4,6}$') seq = tuple(cp for cp in seq if cp != EMOJI_VS)
result = {} zwj_seq_without_vs.add(seq)
for name, dirname in name_to_dirpath.iteritems():
if not name.startswith(prefix): for seq, name in sorted(
print('expected prefix "%s" for "%s"' % (prefix, name)) unicode_data.get_emoji_zwj_sequences(age=age).iteritems()):
continue if EMOJI_VS in seq:
test_seq = tuple(s for s in seq if s != EMOJI_VS)
segments = name[len(prefix): -len(suffix)].split('_') else:
segfail = False test_seq = seq
seq = [] if test_seq not in zwj_seq_without_vs:
for s in segments: print('coverage: missing (canonical) zwj sequence %s (%s)' % (
if not segment_re.match(s): unicode_data.seq_to_string(seq), name))
print('bad codepoint name "%s" in %s/%s' % (s, dirname, name))
segfail = True # check for 'unknown flag'
continue # this is either emoji_ufe82b or 'unknown_flag', but we filter out things that
n = int(s, 16) # don't start with our prefix so 'unknown_flag' would be excluded by default.
if n > 0x10ffff: if tuple([0xfe82b]) not in seq_to_filepath:
print('codepoint "%s" out of range in %s/%s' % (s, dirname, name)) print('coverage: missing unknown flag PUA fe82b')
segfail = True
continue
seq.append(n) def check_sequence_to_filepath(seq_to_filepath, unicode_version, coverage):
if not segfail: sorted_seq_to_filepath = collections.OrderedDict(
result[tuple(seq)] = path.join(dirname, name) sorted(seq_to_filepath.items()))
return result _check_no_vs(sorted_seq_to_filepath)
_check_valid_emoji_cps(sorted_seq_to_filepath, unicode_version)
_check_zwj(sorted_seq_to_filepath)
def collect_name_to_dirpath(directory, prefix, suffix): _check_flags(sorted_seq_to_filepath)
"""Return a mapping from filename to path rooted at directory, ignoring files _check_tags(sorted_seq_to_filepath)
that don't match suffix. Report when a filename appears in more than one _check_skintone(sorted_seq_to_filepath)
subdir; the first path found is kept.""" _check_zwj_sequences(sorted_seq_to_filepath, unicode_version)
result = {} _check_no_alias_sources(sorted_seq_to_filepath)
for dirname, _, files in os.walk(directory): if coverage:
if directory != '.': _check_coverage(sorted_seq_to_filepath, unicode_version)
dirname = path.join(directory, dirname)
for f in files:
if not f.endswith(suffix): def create_sequence_to_filepath(name_to_dirpath, prefix, suffix):
continue """Check names, and convert name to sequences for names that are ok,
if f in result: returning a sequence to file path mapping. Reports bad segments
print('duplicate file "%s" in %s and %s ' % ( of a name to stderr."""
f, dirname, result[f]), file=sys.stderr) segment_re = re.compile(r'^[0-9a-f]{4,6}$')
continue result = {}
result[f] = dirname for name, dirname in name_to_dirpath.iteritems():
return result if not name.startswith(prefix):
print('expected prefix "%s" for "%s"' % (prefix, name))
continue
def collect_name_to_dirpath_with_override(dirs, prefix, suffix):
"""Return a mapping from filename to a directory path rooted at a directory segments = name[len(prefix): -len(suffix)].split('_')
in dirs, using collect_name_to_filepath. The last directory is retained. This segfail = False
does not report an error if a file appears under more than one root directory, seq = []
so lets later root directories override earlier ones.""" for s in segments:
result = {} if not segment_re.match(s):
for d in dirs: print('bad codepoint name "%s" in %s/%s' % (s, dirname, name))
result.update(collect_name_to_dirpath(d, prefix, suffix)) segfail = True
return result continue
n = int(s, 16)
if n > 0x10ffff:
def run_check(dirs, prefix, suffix): print('codepoint "%s" out of range in %s/%s' % (s, dirname, name))
print('Checking files with prefix "%s" and suffix "%s" in:\n %s' % ( segfail = True
prefix, suffix, '\n '.join(dirs))) continue
name_to_dirpath = collect_name_to_dirpath_with_override( seq.append(n)
dirs, prefix=prefix, suffix=suffix) if not segfail:
print('checking %d names' % len(name_to_dirpath)) result[tuple(seq)] = path.join(dirname, name)
seq_to_filepath = create_sequence_to_filepath(name_to_dirpath, prefix, suffix) return result
print('checking %d sequences' % len(seq_to_filepath))
check_sequence_to_filepath(seq_to_filepath)
print('done.') def collect_name_to_dirpath(directory, prefix, suffix, exclude=None):
"""Return a mapping from filename to path rooted at directory, ignoring files
that don't match suffix, and subtrees with names in exclude. Report when a
def main(): filename appears in more than one subdir; the first path found is kept."""
parser = argparse.ArgumentParser() result = {}
parser.add_argument( for dirname, dirs, files in os.walk(directory, topdown=True):
'-d', '--dirs', help='directories containing emoji images', if exclude:
metavar='dir', nargs='+', required=True) dirs[:] = [d for d in dirs if d not in exclude]
parser.add_argument(
'-p', '--prefix', help='prefix to match, default "emoji_u"', if directory != '.':
metavar='pfx', default='emoji_u') dirname = path.join(directory, dirname)
parser.add_argument( for f in files:
'-s', '--suffix', help='suffix to match, default ".png"', metavar='sfx', if not f.endswith(suffix):
default='.png') continue
args = parser.parse_args() if f in result:
run_check(args.dirs, args.prefix, args.suffix) print('duplicate file "%s" in %s and %s ' % (
f, dirname, result[f]), file=sys.stderr)
continue
if __name__ == '__main__': result[f] = dirname
main() return result
def collect_name_to_dirpath_with_override(dirs, prefix, suffix, exclude=None):
"""Return a mapping from filename to a directory path rooted at a directory
in dirs, using collect_name_to_filepath. The last directory is retained. This
does not report an error if a file appears under more than one root directory,
so lets later root directories override earlier ones. Use 'exclude' to
name subdirectories (of any root) whose subtree you wish to skip."""
result = {}
for d in dirs:
result.update(collect_name_to_dirpath(d, prefix, suffix, exclude))
return result
def run_check(dirs, prefix, suffix, exclude, unicode_version, coverage):
msg = ''
if unicode_version:
msg = ' (%3.1f)' % unicode_version
print('Checking files with prefix "%s" and suffix "%s"%s in:\n %s' % (
prefix, suffix, msg, '\n '.join(dirs)))
name_to_dirpath = collect_name_to_dirpath_with_override(
dirs, prefix=prefix, suffix=suffix, exclude=exclude)
print('checking %d names' % len(name_to_dirpath))
seq_to_filepath = create_sequence_to_filepath(name_to_dirpath, prefix, suffix)
print('checking %d sequences' % len(seq_to_filepath))
check_sequence_to_filepath(seq_to_filepath, unicode_version, coverage)
print('done.')
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'-d', '--dirs', help='directory roots containing emoji images',
metavar='dir', nargs='+', required=True)
parser.add_argument(
'-e', '--exclude', help='names of source subdirs to exclude',
metavar='dir', nargs='+')
parser.add_argument(
'-c', '--coverage', help='test for complete coverage',
action='store_true')
parser.add_argument(
'-p', '--prefix', help='prefix to match, default "emoji_u"',
metavar='pfx', default='emoji_u')
parser.add_argument(
'-s', '--suffix', help='suffix to match, default ".png"', metavar='sfx',
default='.png')
parser.add_argument(
'-u', '--unicode_version', help='limit to this unicode version or before',
metavar='version', type=float)
args = parser.parse_args()
run_check(
args.dirs, args.prefix, args.suffix, args.exclude, args.unicode_version,
args.coverage)
if __name__ == '__main__':
main()

View file

@ -1,150 +1,150 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright 2015 Google, Inc. All Rights Reserved. # Copyright 2015 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Doug Felt # Google Author(s): Doug Felt
"""Tool to collect emoji svg glyphs into one directory for processing """Tool to collect emoji svg glyphs into one directory for processing
by add_svg_glyphs. There are two sources, noto/color_emoji/svg and by add_svg_glyphs. There are two sources, noto/color_emoji/svg and
noto/third_party/region-flags/svg. The add_svg_glyphs file expects noto/third_party/region-flags/svg. The add_svg_glyphs file expects
the file names to contain the character string that represents it the file names to contain the character string that represents it
represented as a sequence of hex-encoded codepoints separated by represented as a sequence of hex-encoded codepoints separated by
underscore. The files in noto/color_emoji/svg do this, and have the underscore. The files in noto/color_emoji/svg do this, and have the
prefix 'emoji_u', but the files in region-flags/svg just have the prefix 'emoji_u', but the files in region-flags/svg just have the
two-letter code. two-letter code.
We create a directory and copy the files into it with the required We create a directory and copy the files into it with the required
naming convention. First we do this for region-flags/svg, converting naming convention. First we do this for region-flags/svg, converting
the names, and then we do this for color_emoji/svg, so any duplicates the names, and then we do this for color_emoji/svg, so any duplicates
will be overwritten by what we assume are the preferred svg. We use will be overwritten by what we assume are the preferred svg. We use
copies instead of symlinks so we can continue to optimize or modify copies instead of symlinks so we can continue to optimize or modify
the files without messing with the originals.""" the files without messing with the originals."""
import argparse import argparse
import glob import glob
import logging import logging
import os import os
import os.path import os.path
import re import re
import shutil import shutil
import sys import sys
from nototools import tool_utils from nototools import tool_utils
def _is_svg(f): def _is_svg(f):
return f.endswith('.svg') return f.endswith('.svg')
def _is_svg_and_startswith_emoji(f): def _is_svg_and_startswith_emoji(f):
return f.endswith('.svg') and f.startswith('emoji_u') return f.endswith('.svg') and f.startswith('emoji_u')
def _flag_rename(f): def _flag_rename(f):
"""Converts a file name from two-letter upper-case ASCII to our expected """Converts a file name from two-letter upper-case ASCII to our expected
'emoji_uXXXXX_XXXXX form, mapping each character to the corresponding 'emoji_uXXXXX_XXXXX form, mapping each character to the corresponding
regional indicator symbol.""" regional indicator symbol."""
cp_strs = [] cp_strs = []
name, ext = os.path.splitext(f) name, ext = os.path.splitext(f)
if len(name) != 2: if len(name) != 2:
raise ValueError('illegal flag name "%s"' % f) raise ValueError('illegal flag name "%s"' % f)
for cp in name: for cp in name:
if not ('A' <= cp <= 'Z'): if not ('A' <= cp <= 'Z'):
raise ValueError('illegal flag name "%s"' % f) raise ValueError('illegal flag name "%s"' % f)
ncp = 0x1f1e6 - 0x41 + ord(cp) ncp = 0x1f1e6 - 0x41 + ord(cp)
cp_strs.append("%04x" % ncp) cp_strs.append("%04x" % ncp)
return 'emoji_u%s%s' % ('_'.join(cp_strs), ext) return 'emoji_u%s%s' % ('_'.join(cp_strs), ext)
def copy_with_rename(src_dir, dst_dir, accept_pred=None, rename=None): def copy_with_rename(src_dir, dst_dir, accept_pred=None, rename=None):
"""Copy files from src_dir to dst_dir that match accept_pred (all if None) and """Copy files from src_dir to dst_dir that match accept_pred (all if None) and
rename using rename (if not None), replacing existing files. accept_pred rename using rename (if not None), replacing existing files. accept_pred
takes the filename and returns True if the file should be copied, rename takes takes the filename and returns True if the file should be copied, rename takes
the filename and returns a new file name.""" the filename and returns a new file name."""
count = 0 count = 0
replace_count = 0 replace_count = 0
for src_filename in os.listdir(src_dir): for src_filename in os.listdir(src_dir):
if accept_pred and not accept_pred(src_filename): if accept_pred and not accept_pred(src_filename):
continue continue
dst_filename = rename(src_filename) if rename else src_filename dst_filename = rename(src_filename) if rename else src_filename
src = os.path.join(src_dir, src_filename) src = os.path.join(src_dir, src_filename)
dst = os.path.join(dst_dir, dst_filename) dst = os.path.join(dst_dir, dst_filename)
if os.path.exists(dst): if os.path.exists(dst):
logging.debug('Replacing existing file %s', dst) logging.debug('Replacing existing file %s', dst)
os.unlink(dst) os.unlink(dst)
replace_count += 1 replace_count += 1
shutil.copy2(src, dst) shutil.copy2(src, dst)
logging.debug('cp -p %s %s', src, dst) logging.debug('cp -p %s %s', src, dst)
count += 1 count += 1
if logging.getLogger().getEffectiveLevel() <= logging.INFO: if logging.getLogger().getEffectiveLevel() <= logging.INFO:
src_short = tool_utils.short_path(src_dir) src_short = tool_utils.short_path(src_dir)
dst_short = tool_utils.short_path(dst_dir) dst_short = tool_utils.short_path(dst_dir)
logging.info('Copied %d files (replacing %d) from %s to %s', logging.info('Copied %d files (replacing %d) from %s to %s',
count, replace_count, src_short, dst_short) count, replace_count, src_short, dst_short)
def build_svg_dir(dst_dir, clean=False, emoji_dir='', flags_dir=''): def build_svg_dir(dst_dir, clean=False, emoji_dir='', flags_dir=''):
"""Copies/renames files from emoji_dir and then flags_dir, giving them the """Copies/renames files from emoji_dir and then flags_dir, giving them the
standard format and prefix ('emoji_u' followed by codepoints expressed in hex standard format and prefix ('emoji_u' followed by codepoints expressed in hex
separated by underscore). If clean, removes the target dir before proceding. separated by underscore). If clean, removes the target dir before proceding.
If either emoji_dir or flags_dir are empty, skips them.""" If either emoji_dir or flags_dir are empty, skips them."""
dst_dir = tool_utils.ensure_dir_exists(dst_dir, clean=clean) dst_dir = tool_utils.ensure_dir_exists(dst_dir, clean=clean)
if not emoji_dir and not flags_dir: if not emoji_dir and not flags_dir:
logging.warning('Nothing to do.') logging.warning('Nothing to do.')
return return
if emoji_dir: if emoji_dir:
copy_with_rename( copy_with_rename(
emoji_dir, dst_dir, accept_pred=_is_svg_and_startswith_emoji) emoji_dir, dst_dir, accept_pred=_is_svg_and_startswith_emoji)
if flags_dir: if flags_dir:
copy_with_rename( copy_with_rename(
flags_dir, dst_dir, accept_pred=_is_svg, rename=_flag_rename) flags_dir, dst_dir, accept_pred=_is_svg, rename=_flag_rename)
def main(argv): def main(argv):
DEFAULT_EMOJI_DIR = '[emoji]/svg' DEFAULT_EMOJI_DIR = '[emoji]/svg'
DEFAULT_FLAGS_DIR = '[emoji]/third_party/region-flags/svg' DEFAULT_FLAGS_DIR = '[emoji]/third_party/region-flags/svg'
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Collect svg files into target directory with prefix.') description='Collect svg files into target directory with prefix.')
parser.add_argument( parser.add_argument(
'dst_dir', help='Directory to hold copied files.', metavar='dir') 'dst_dir', help='Directory to hold copied files.', metavar='dir')
parser.add_argument( parser.add_argument(
'--clean', '-c', help='Replace target directory', action='store_true') '--clean', '-c', help='Replace target directory', action='store_true')
parser.add_argument( parser.add_argument(
'--flags_dir', '-f', metavar='dir', help='directory containing flag svg, ' '--flags_dir', '-f', metavar='dir', help='directory containing flag svg, '
'default %s' % DEFAULT_FLAGS_DIR, default=DEFAULT_FLAGS_DIR) 'default %s' % DEFAULT_FLAGS_DIR, default=DEFAULT_FLAGS_DIR)
parser.add_argument( parser.add_argument(
'--emoji_dir', '-e', metavar='dir', '--emoji_dir', '-e', metavar='dir',
help='directory containing emoji svg, default %s' % DEFAULT_EMOJI_DIR, help='directory containing emoji svg, default %s' % DEFAULT_EMOJI_DIR,
default=DEFAULT_EMOJI_DIR) default=DEFAULT_EMOJI_DIR)
parser.add_argument( parser.add_argument(
'-l', '--loglevel', help='log level name/value', default='warning') '-l', '--loglevel', help='log level name/value', default='warning')
args = parser.parse_args(argv) args = parser.parse_args(argv)
tool_utils.setup_logging(args.loglevel) tool_utils.setup_logging(args.loglevel)
args.flags_dir = tool_utils.resolve_path(args.flags_dir) args.flags_dir = tool_utils.resolve_path(args.flags_dir)
args.emoji_dir = tool_utils.resolve_path(args.emoji_dir) args.emoji_dir = tool_utils.resolve_path(args.emoji_dir)
build_svg_dir( build_svg_dir(
args.dst_dir, clean=args.clean, emoji_dir=args.emoji_dir, args.dst_dir, clean=args.clean, emoji_dir=args.emoji_dir,
flags_dir=args.flags_dir) flags_dir=args.flags_dir)
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv[1:]) main(sys.argv[1:])

Binary file not shown.

Binary file not shown.

View file

@ -1,56 +1,56 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2014 Google Inc. All rights reserved. # Copyright 2014 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Generate a glyph name for flag emojis.""" """Generate a glyph name for flag emojis."""
from __future__ import print_function from __future__ import print_function
__author__ = 'roozbeh@google.com (Roozbeh Pournader)' __author__ = 'roozbeh@google.com (Roozbeh Pournader)'
import re import re
import sys import sys
import add_emoji_gsub import add_emoji_gsub
def two_letter_code_to_glyph_name(region_code): def two_letter_code_to_glyph_name(region_code):
return 'u%04x_%04x' % ( return 'u%04x_%04x' % (
add_emoji_gsub.reg_indicator(region_code[0]), add_emoji_gsub.reg_indicator(region_code[0]),
add_emoji_gsub.reg_indicator(region_code[1])) add_emoji_gsub.reg_indicator(region_code[1]))
subcode_re = re.compile(r'[0-9a-z]{2}-[0-9a-z]+$') subcode_re = re.compile(r'[0-9a-z]{2}-[0-9a-z]+$')
def hyphenated_code_to_glyph_name(sub_code): def hyphenated_code_to_glyph_name(sub_code):
# Hyphenated codes use tag sequences, not regional indicator symbol pairs. # Hyphenated codes use tag sequences, not regional indicator symbol pairs.
sub_code = sub_code.lower() sub_code = sub_code.lower()
if not subcode_re.match(sub_code): if not subcode_re.match(sub_code):
raise Exception('%s is not a valid flag subcode' % sub_code) raise Exception('%s is not a valid flag subcode' % sub_code)
cps = ['u1f3f4'] cps = ['u1f3f4']
cps.extend('e00%02x' % ord(cp) for cp in sub_code if cp != '-') cps.extend('e00%02x' % ord(cp) for cp in sub_code if cp != '-')
cps.append('e007f') cps.append('e007f')
return '_'.join(cps) return '_'.join(cps)
def flag_code_to_glyph_name(flag_code): def flag_code_to_glyph_name(flag_code):
if '-' in flag_code: if '-' in flag_code:
return hyphenated_code_to_glyph_name(flag_code) return hyphenated_code_to_glyph_name(flag_code)
return two_letter_code_to_glyph_name(flag_code) return two_letter_code_to_glyph_name(flag_code)
def main(): def main():
print(' '.join([ print(' '.join([
flag_code_to_glyph_name(flag_code) for flag_code in sys.argv[1:]])) flag_code_to_glyph_name(flag_code) for flag_code in sys.argv[1:]]))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,85 +1,85 @@
#!/usr/bin/python #!/usr/bin/python3
# #
# Copyright 2016 Google Inc. All rights reserved. # Copyright 2016 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Quick tool to display count/ids of flag images in a directory named """Quick tool to display count/ids of flag images in a directory named
either using ASCII upper case pairs or the emoji_u+codepoint_sequence either using ASCII upper case pairs or the emoji_u+codepoint_sequence
names.""" names."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import re import re
import glob import glob
import os import os
from os import path from os import path
def _flag_names_from_emoji_file_names(src): def _flag_names_from_emoji_file_names(src):
def _flag_char(char_str): def _flag_char(char_str):
return unichr(ord('A') + int(char_str, 16) - 0x1f1e6) return unichr(ord('A') + int(char_str, 16) - 0x1f1e6)
flag_re = re.compile('emoji_u(1f1[0-9a-f]{2})_(1f1[0-9a-f]{2}).png') flag_re = re.compile('emoji_u(1f1[0-9a-f]{2})_(1f1[0-9a-f]{2}).png')
flags = set() flags = set()
for f in glob.glob(path.join(src, 'emoji_u*.png')): for f in glob.glob(path.join(src, 'emoji_u*.png')):
m = flag_re.match(path.basename(f)) m = flag_re.match(path.basename(f))
if not m: if not m:
continue continue
flag_short_name = _flag_char(m.group(1)) + _flag_char(m.group(2)) flag_short_name = _flag_char(m.group(1)) + _flag_char(m.group(2))
flags.add(flag_short_name) flags.add(flag_short_name)
return flags return flags
def _flag_names_from_file_names(src): def _flag_names_from_file_names(src):
flag_re = re.compile('([A-Z]{2}).png') flag_re = re.compile('([A-Z]{2}).png')
flags = set() flags = set()
for f in glob.glob(path.join(src, '*.png')): for f in glob.glob(path.join(src, '*.png')):
m = flag_re.match(path.basename(f)) m = flag_re.match(path.basename(f))
if not m: if not m:
print('no match') print('no match')
continue continue
flags.add(m.group(1)) flags.add(m.group(1))
return flags return flags
def _dump_flag_info(names): def _dump_flag_info(names):
prev = None prev = None
print('%d flags' % len(names)) print('%d flags' % len(names))
for n in sorted(names): for n in sorted(names):
if n[0] != prev: if n[0] != prev:
if prev: if prev:
print() print()
prev = n[0] prev = n[0]
print(n, end=' ') print(n, end=' ')
print() print()
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-s', '--srcdir', help='location of files', metavar='dir', '-s', '--srcdir', help='location of files', metavar='dir',
required=True) required=True)
parser.add_argument( parser.add_argument(
'-n', '--name_type', help='type of names', metavar='type', '-n', '--name_type', help='type of names', metavar='type',
choices=['ascii', 'codepoint'], required=True) choices=['ascii', 'codepoint'], required=True)
args = parser.parse_args() args = parser.parse_args()
if args.name_type == 'ascii': if args.name_type == 'ascii':
names = _flag_names_from_file_names(args.srcdir) names = _flag_names_from_file_names(args.srcdir)
else: else:
names = _flag_names_from_emoji_file_names(args.srcdir) names = _flag_names_from_emoji_file_names(args.srcdir)
print(args.srcdir) print(args.srcdir)
_dump_flag_info(names) _dump_flag_info(names)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

Binary file not shown.

Binary file not shown.

View file

@ -1,192 +1,192 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2015 Google Inc. All rights reserved. # Copyright 2015 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Generate version string for NotoColorEmoji. """Generate version string for NotoColorEmoji.
This parses the color emoji template file and updates the lines This parses the color emoji template file and updates the lines
containing version string info, writing a new file. containing version string info, writing a new file.
The nameID 5 field in the emoji font should reflect the commit/date The nameID 5 field in the emoji font should reflect the commit/date
of the repo it was built from. This will build a string of the following of the repo it was built from. This will build a string of the following
format: format:
Version 1.39;GOOG;noto-emoji:20170220:a8a215d2e889' Version 1.39;GOOG;noto-emoji:20170220:a8a215d2e889'
This is intended to indicate that it was built by Google from noto-emoji This is intended to indicate that it was built by Google from noto-emoji
at commit a8a215d2e889 and date 20170220 (since dates are a bit easier at commit a8a215d2e889 and date 20170220 (since dates are a bit easier
to locate in time than commit hashes). to locate in time than commit hashes).
For building with external data we don't include the commit id as we For building with external data we don't include the commit id as we
might be using different resoruces. Instead the version string is: might be using different resoruces. Instead the version string is:
Version 1.39;GOOG;noto-emoji:20170518;BETA <msg> Version 1.39;GOOG;noto-emoji:20170518;BETA <msg>
Here the date is the current date, and the message after 'BETA ' is Here the date is the current date, and the message after 'BETA ' is
provided using the '-b' flag. There's no commit hash. This also provided using the '-b' flag. There's no commit hash. This also
bypasses some checks about the state of the repo. bypasses some checks about the state of the repo.
The relase number should have 2 or 3 minor digits. Right now we've been The relase number should have 2 or 3 minor digits. Right now we've been
using 2 but at the next major relase we probably want to use 3. This using 2 but at the next major relase we probably want to use 3. This
supports both. It will bump the version number if none is provided, supports both. It will bump the version number if none is provided,
maintaining the minor digit length. maintaining the minor digit length.
""" """
import argparse import argparse
import datetime import datetime
import re import re
from nototools import tool_utils from nototools import tool_utils
# These are not very lenient, we expect to be applied to the noto color # These are not very lenient, we expect to be applied to the noto color
# emoji template ttx file which matches these. Why then require the # emoji template ttx file which matches these. Why then require the
# input argument, you ask? Um... testing? # input argument, you ask? Um... testing?
_nameid_re = re.compile(r'\s*<namerecord nameID="5"') _nameid_re = re.compile(r'\s*<namerecord nameID="5"')
_version_re = re.compile(r'\s*Version\s(\d+.\d{2,3})') _version_re = re.compile(r'\s*Version\s(\d+.\d{2,3})')
_headrev_re = re.compile(r'\s*<fontRevision value="(\d+.\d{2,3})"/>') _headrev_re = re.compile(r'\s*<fontRevision value="(\d+.\d{2,3})"/>')
def _get_existing_version(lines): def _get_existing_version(lines):
"""Scan lines for all existing version numbers, and ensure they match. """Scan lines for all existing version numbers, and ensure they match.
Return the matched version number string.""" Return the matched version number string."""
version = None version = None
def check_version(new_version): def check_version(new_version):
if version is not None and new_version != version: if version is not None and new_version != version:
raise Exception( raise Exception(
'version %s and namerecord version %s do not match' % ( 'version %s and namerecord version %s do not match' % (
version, new_version)) version, new_version))
return new_version return new_version
saw_nameid = False saw_nameid = False
for line in lines: for line in lines:
if saw_nameid: if saw_nameid:
saw_nameid = False saw_nameid = False
m = _version_re.match(line) m = _version_re.match(line)
if not m: if not m:
raise Exception('could not match line "%s" in namerecord' % line) raise Exception('could not match line "%s" in namerecord' % line)
version = check_version(m.group(1)) version = check_version(m.group(1))
elif _nameid_re.match(line): elif _nameid_re.match(line):
saw_nameid = True saw_nameid = True
else: else:
m = _headrev_re.match(line) m = _headrev_re.match(line)
if m: if m:
version = check_version(m.group(1)) version = check_version(m.group(1))
return version return version
def _version_to_mm(version): def _version_to_mm(version):
majs, mins = version.split('.') majs, mins = version.split('.')
minor_len = len(mins) minor_len = len(mins)
return int(majs), int(mins), minor_len return int(majs), int(mins), minor_len
def _mm_to_version(major, minor, minor_len): def _mm_to_version(major, minor, minor_len):
fmt = '%%d.%%0%dd' % minor_len fmt = '%%d.%%0%dd' % minor_len
return fmt % (major, minor) return fmt % (major, minor)
def _version_compare(lhs, rhs): def _version_compare(lhs, rhs):
lmaj, lmin, llen = _version_to_mm(lhs) lmaj, lmin, llen = _version_to_mm(lhs)
rmaj, rmin, rlen = _version_to_mm(rhs) rmaj, rmin, rlen = _version_to_mm(rhs)
# if major versions differ, we don't care about the minor length, else # if major versions differ, we don't care about the minor length, else
# they should be the same # they should be the same
if lmaj != rmaj: if lmaj != rmaj:
return lmaj - rmaj return lmaj - rmaj
if llen != rlen: if llen != rlen:
raise Exception('minor version lengths differ: "%s" and "%s"' % (lhs, rhs)) raise Exception('minor version lengths differ: "%s" and "%s"' % (lhs, rhs))
return lmin - rmin return lmin - rmin
def _version_bump(version): def _version_bump(version):
major, minor, minor_len = _version_to_mm(version) major, minor, minor_len = _version_to_mm(version)
minor = (minor + 1) % (10 ** minor_len) minor = (minor + 1) % (10 ** minor_len)
if minor == 0: if minor == 0:
raise Exception('cannot bump version "%s", requires new major' % version) raise Exception('cannot bump version "%s", requires new major' % version)
return _mm_to_version(major, minor, minor_len) return _mm_to_version(major, minor, minor_len)
def _get_repo_version_str(beta): def _get_repo_version_str(beta):
"""See above for description of this string.""" """See above for description of this string."""
if beta is not None: if beta is not None:
date_str = datetime.date.today().strftime('%Y%m%d') date_str = datetime.date.today().strftime('%Y%m%d')
return 'GOOG;noto-emoji:%s;BETA %s' % (date_str, beta) return 'GOOG;noto-emoji:%s;BETA %s' % (date_str, beta)
p = tool_utils.resolve_path('[emoji]') p = tool_utils.resolve_path('[emoji]')
commit, date, _ = tool_utils.git_head_commit(p) commit, date, _ = tool_utils.git_head_commit(p)
if not tool_utils.git_check_remote_commit(p, commit): if not tool_utils.git_check_remote_commit(p, commit):
raise Exception('emoji not on upstream master branch') raise Exception('emoji not on upstream master branch')
date_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})') date_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})')
m = date_re.match(date) m = date_re.match(date)
if not m: if not m:
raise Exception('could not match "%s" with "%s"' % (date, date_re.pattern)) raise Exception('could not match "%s" with "%s"' % (date, date_re.pattern))
ymd = ''.join(m.groups()) ymd = ''.join(m.groups())
return 'GOOG;noto-emoji:%s:%s' % (ymd, commit[:12]) return 'GOOG;noto-emoji:%s:%s' % (ymd, commit[:12])
def _replace_existing_version(lines, version, version_str): def _replace_existing_version(lines, version, version_str):
"""Update lines with new version strings in appropriate places.""" """Update lines with new version strings in appropriate places."""
saw_nameid = False saw_nameid = False
for i in range(len(lines)): for i in range(len(lines)):
line = lines[i] line = lines[i]
if saw_nameid: if saw_nameid:
saw_nameid = False saw_nameid = False
# preserve indentation # preserve indentation
lead_ws = len(line) - len(line.lstrip()) lead_ws = len(line) - len(line.lstrip())
lines[i] = line[:lead_ws] + version_str + '\n' lines[i] = line[:lead_ws] + version_str + '\n'
elif _nameid_re.match(line): elif _nameid_re.match(line):
saw_nameid = True saw_nameid = True
elif _headrev_re.match(line): elif _headrev_re.match(line):
lead_ws = len(line) - len(line.lstrip()) lead_ws = len(line) - len(line.lstrip())
lines[i] = line[:lead_ws] + '<fontRevision value="%s"/>\n' % version lines[i] = line[:lead_ws] + '<fontRevision value="%s"/>\n' % version
def update_version(srcfile, dstfile, version, beta): def update_version(srcfile, dstfile, version, beta):
"""Update version in srcfile and write to dstfile. If version is None, """Update version in srcfile and write to dstfile. If version is None,
bumps the current version, else version must be greater than the bumps the current version, else version must be greater than the
current verison.""" current verison."""
with open(srcfile, 'r') as f: with open(srcfile, 'r') as f:
lines = f.readlines() lines = f.readlines()
current_version = _get_existing_version(lines) current_version = _get_existing_version(lines)
if not version: if not version:
version = _version_bump(current_version) version = _version_bump(current_version)
elif version and _version_compare(version, current_version) <= 0: elif version and _version_compare(version, current_version) <= 0:
raise Exception('new version %s is <= current version %s' % ( raise Exception('new version %s is <= current version %s' % (
version, current_version)) version, current_version))
version_str = 'Version %s;%s' % (version, _get_repo_version_str(beta)) version_str = 'Version %s;%s' % (version, _get_repo_version_str(beta))
_replace_existing_version(lines, version, version_str) _replace_existing_version(lines, version, version_str)
with open(dstfile, 'w') as f: with open(dstfile, 'w') as f:
for line in lines: for line in lines:
f.write(line) f.write(line)
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-v', '--version', help='version number, default bumps the current ' '-v', '--version', help='version number, default bumps the current '
'version', metavar='ver') 'version', metavar='ver')
parser.add_argument( parser.add_argument(
'-s', '--src', help='ttx file with name and head tables', '-s', '--src', help='ttx file with name and head tables',
metavar='file', required=True) metavar='file', required=True)
parser.add_argument( parser.add_argument(
'-d', '--dst', help='name of edited ttx file to write', '-d', '--dst', help='name of edited ttx file to write',
metavar='file', required=True) metavar='file', required=True)
parser.add_argument( parser.add_argument(
'-b', '--beta', help='beta tag if font is built using external resources') '-b', '--beta', help='beta tag if font is built using external resources')
args = parser.parse_args() args = parser.parse_args()
update_version(args.src, args.dst, args.version, args.beta) update_version(args.src, args.dst, args.version, args.beta)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

File diff suppressed because it is too large Load diff

View file

@ -1,405 +1,405 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-# # -*- coding: utf-8 -*-#
# #
# Copyright 2015 Google Inc. All rights reserved. # Copyright 2015 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Generate name data for emoji resources. Currently in json format.""" """Generate name data for emoji resources. Currently in json format."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import collections import collections
import glob import glob
import json import json
import os import os
from os import path from os import path
import re import re
import sys import sys
import generate_emoji_html import generate_emoji_html
from nototools import tool_utils from nototools import tool_utils
from nototools import unicode_data from nototools import unicode_data
def _create_custom_gendered_seq_names(): def _create_custom_gendered_seq_names():
"""The names have detail that is adequately represented by the image.""" """The names have detail that is adequately represented by the image."""
BOY = 0x1f466 BOY = 0x1f466
GIRL = 0x1f467 GIRL = 0x1f467
MAN = 0x1f468 MAN = 0x1f468
WOMAN = 0x1f469 WOMAN = 0x1f469
HEART = 0x2764 # Heavy Black Heart HEART = 0x2764 # Heavy Black Heart
KISS_MARK = 0x1f48b KISS_MARK = 0x1f48b
return { return {
(MAN, HEART, KISS_MARK, MAN): 'Kiss', (MAN, HEART, KISS_MARK, MAN): 'Kiss',
(WOMAN, HEART, KISS_MARK, WOMAN): 'Kiss', (WOMAN, HEART, KISS_MARK, WOMAN): 'Kiss',
(WOMAN, HEART, KISS_MARK, MAN): 'Kiss', (WOMAN, HEART, KISS_MARK, MAN): 'Kiss',
(WOMAN, HEART, MAN): 'Couple with Heart', (WOMAN, HEART, MAN): 'Couple with Heart',
(MAN, HEART, MAN): 'Couple with Heart', (MAN, HEART, MAN): 'Couple with Heart',
(WOMAN, HEART, WOMAN): 'Couple with Heart', (WOMAN, HEART, WOMAN): 'Couple with Heart',
(MAN, GIRL): 'Family', (MAN, GIRL): 'Family',
(MAN, GIRL, GIRL): 'Family', (MAN, GIRL, GIRL): 'Family',
(MAN, GIRL, BOY): 'Family', (MAN, GIRL, BOY): 'Family',
(MAN, BOY): 'Family', (MAN, BOY): 'Family',
(MAN, BOY, BOY): 'Family', (MAN, BOY, BOY): 'Family',
(MAN, WOMAN, GIRL): 'Family', (MAN, WOMAN, GIRL): 'Family',
(MAN, WOMAN, GIRL, GIRL): 'Family', (MAN, WOMAN, GIRL, GIRL): 'Family',
(MAN, WOMAN, GIRL, BOY): 'Family', (MAN, WOMAN, GIRL, BOY): 'Family',
(MAN, WOMAN, BOY): 'Family', (MAN, WOMAN, BOY): 'Family',
(MAN, WOMAN, BOY, BOY): 'Family', (MAN, WOMAN, BOY, BOY): 'Family',
(MAN, MAN, GIRL): 'Family', (MAN, MAN, GIRL): 'Family',
(MAN, MAN, GIRL, GIRL): 'Family', (MAN, MAN, GIRL, GIRL): 'Family',
(MAN, MAN, GIRL, BOY): 'Family', (MAN, MAN, GIRL, BOY): 'Family',
(MAN, MAN, BOY): 'Family', (MAN, MAN, BOY): 'Family',
(MAN, MAN, BOY, BOY): 'Family', (MAN, MAN, BOY, BOY): 'Family',
(WOMAN, GIRL): 'Family', (WOMAN, GIRL): 'Family',
(WOMAN, GIRL, GIRL): 'Family', (WOMAN, GIRL, GIRL): 'Family',
(WOMAN, GIRL, BOY): 'Family', (WOMAN, GIRL, BOY): 'Family',
(WOMAN, BOY): 'Family', (WOMAN, BOY): 'Family',
(WOMAN, BOY, BOY): 'Family', (WOMAN, BOY, BOY): 'Family',
(WOMAN, WOMAN, GIRL): 'Family', (WOMAN, WOMAN, GIRL): 'Family',
(WOMAN, WOMAN, GIRL, GIRL): 'Family', (WOMAN, WOMAN, GIRL, GIRL): 'Family',
(WOMAN, WOMAN, GIRL, BOY): 'Family', (WOMAN, WOMAN, GIRL, BOY): 'Family',
(WOMAN, WOMAN, BOY): 'Family', (WOMAN, WOMAN, BOY): 'Family',
(WOMAN, WOMAN, BOY, BOY): 'Family' } (WOMAN, WOMAN, BOY, BOY): 'Family' }
def _create_custom_seq_names(): def _create_custom_seq_names():
"""These have names that often are of the form 'Person xyz-ing' or 'Man Xyz.' """These have names that often are of the form 'Person xyz-ing' or 'Man Xyz.'
We opt to simplify the former to an activity name or action, and the latter to We opt to simplify the former to an activity name or action, and the latter to
drop the gender. This also generally makes the names shorter.""" drop the gender. This also generally makes the names shorter."""
EYE = 0x1f441 EYE = 0x1f441
SPEECH = 0x1f5e8 SPEECH = 0x1f5e8
WHITE_FLAG = 0x1f3f3 WHITE_FLAG = 0x1f3f3
RAINBOW = 0x1f308 RAINBOW = 0x1f308
return { return {
(EYE, SPEECH): 'I Witness', (EYE, SPEECH): 'I Witness',
(WHITE_FLAG, RAINBOW): 'Rainbow Flag', (WHITE_FLAG, RAINBOW): 'Rainbow Flag',
(0x2695,): 'Health Worker', (0x2695,): 'Health Worker',
(0x2696,): 'Judge', (0x2696,): 'Judge',
(0x26f7,): 'Skiing', (0x26f7,): 'Skiing',
(0x26f9,): 'Bouncing a Ball', (0x26f9,): 'Bouncing a Ball',
(0x2708,): 'Pilot', (0x2708,): 'Pilot',
(0x1f33e,): 'Farmer', (0x1f33e,): 'Farmer',
(0x1f373,): 'Cook', (0x1f373,): 'Cook',
(0x1f393,): 'Student', (0x1f393,): 'Student',
(0x1f3a4,): 'Singer', (0x1f3a4,): 'Singer',
(0x1f3a8,): 'Artist', (0x1f3a8,): 'Artist',
(0x1f3c2,): 'Snowboarding', (0x1f3c2,): 'Snowboarding',
(0x1f3c3,): 'Running', (0x1f3c3,): 'Running',
(0x1f3c4,): 'Surfing', (0x1f3c4,): 'Surfing',
(0x1f3ca,): 'Swimming', (0x1f3ca,): 'Swimming',
(0x1f3cb,): 'Weight Lifting', (0x1f3cb,): 'Weight Lifting',
(0x1f3cc,): 'Golfing', (0x1f3cc,): 'Golfing',
(0x1f3eb,): 'Teacher', (0x1f3eb,): 'Teacher',
(0x1f3ed,): 'Factory Worker', (0x1f3ed,): 'Factory Worker',
(0x1f46e,): 'Police Officer', (0x1f46e,): 'Police Officer',
(0x1f46f,): 'Partying', (0x1f46f,): 'Partying',
(0x1f471,): 'Person with Blond Hair', (0x1f471,): 'Person with Blond Hair',
(0x1f473,): 'Person Wearing Turban', (0x1f473,): 'Person Wearing Turban',
(0x1f477,): 'Construction Worker', (0x1f477,): 'Construction Worker',
(0x1f481,): 'Tipping Hand', (0x1f481,): 'Tipping Hand',
(0x1f482,): 'Guard', (0x1f482,): 'Guard',
(0x1f486,): 'Face Massage', (0x1f486,): 'Face Massage',
(0x1f487,): 'Haircut', (0x1f487,): 'Haircut',
(0x1f4bb,): 'Technologist', (0x1f4bb,): 'Technologist',
(0x1f4bc,): 'Office Worker', (0x1f4bc,): 'Office Worker',
(0x1f527,): 'Mechanic', (0x1f527,): 'Mechanic',
(0x1f52c,): 'Scientist', (0x1f52c,): 'Scientist',
(0x1f575,): 'Detective', (0x1f575,): 'Detective',
(0x1f645,): 'No Good Gesture', (0x1f645,): 'No Good Gesture',
(0x1f646,): 'OK Gesture', (0x1f646,): 'OK Gesture',
(0x1f647,): 'Bowing Deeply', (0x1f647,): 'Bowing Deeply',
(0x1f64b,): 'Raising Hand', (0x1f64b,): 'Raising Hand',
(0x1f64d,): 'Frowning', (0x1f64d,): 'Frowning',
(0x1f64e,): 'Pouting', (0x1f64e,): 'Pouting',
(0x1f680,): 'Astronaut', (0x1f680,): 'Astronaut',
(0x1f692,): 'Firefighter', (0x1f692,): 'Firefighter',
(0x1f6a3,): 'Rowing', (0x1f6a3,): 'Rowing',
(0x1f6b4,): 'Bicycling', (0x1f6b4,): 'Bicycling',
(0x1f6b5,): 'Mountain Biking', (0x1f6b5,): 'Mountain Biking',
(0x1f6b6,): 'Walking', (0x1f6b6,): 'Walking',
(0x1f926,): 'Face Palm', (0x1f926,): 'Face Palm',
(0x1f937,): 'Shrug', (0x1f937,): 'Shrug',
(0x1f938,): 'Doing a Cartwheel', (0x1f938,): 'Doing a Cartwheel',
(0x1f939,): 'Juggling', (0x1f939,): 'Juggling',
(0x1f93c,): 'Wrestling', (0x1f93c,): 'Wrestling',
(0x1f93d,): 'Water Polo', (0x1f93d,): 'Water Polo',
(0x1f93e,): 'Playing Handball', (0x1f93e,): 'Playing Handball',
(0x1f9d6,): 'Person in Steamy Room', (0x1f9d6,): 'Person in Steamy Room',
(0x1f9d7,): 'Climbing', (0x1f9d7,): 'Climbing',
(0x1f9d8,): 'Person in Lotus Position', (0x1f9d8,): 'Person in Lotus Position',
(0x1f9d9,): 'Mage', (0x1f9d9,): 'Mage',
(0x1f9da,): 'Fairy', (0x1f9da,): 'Fairy',
(0x1f9db,): 'Vampire', (0x1f9db,): 'Vampire',
(0x1f9dd,): 'Elf', (0x1f9dd,): 'Elf',
(0x1f9de,): 'Genie', (0x1f9de,): 'Genie',
(0x1f9df,): 'Zombie', (0x1f9df,): 'Zombie',
} }
_CUSTOM_GENDERED_SEQ_NAMES = _create_custom_gendered_seq_names() _CUSTOM_GENDERED_SEQ_NAMES = _create_custom_gendered_seq_names()
_CUSTOM_SEQ_NAMES = _create_custom_seq_names() _CUSTOM_SEQ_NAMES = _create_custom_seq_names()
# Fixes for unusual capitalization or cases we don't care to handle in code. # Fixes for unusual capitalization or cases we don't care to handle in code.
# Also prevents titlecasing 'S' after apostrophe in posessives. Note we _do_ # Also prevents titlecasing 'S' after apostrophe in posessives. Note we _do_
# want titlecasing after apostrophe in some cases, e.g. O'Clock. # want titlecasing after apostrophe in some cases, e.g. O'Clock.
_CUSTOM_CAPS_NAMES = { _CUSTOM_CAPS_NAMES = {
(0x26d1,): 'Rescue Workers Helmet', (0x26d1,): 'Rescue Workers Helmet',
(0x1f170,): 'A Button (blood type)', # a Button (Blood Type) (0x1f170,): 'A Button (blood type)', # a Button (Blood Type)
(0x1f171,): 'B Button (blood type)', # B Button (Blood Type) (0x1f171,): 'B Button (blood type)', # B Button (Blood Type)
(0x1f17e,): 'O Button (blood type)', # O Button (Blood Type) (0x1f17e,): 'O Button (blood type)', # O Button (Blood Type)
(0x1f18e,): 'AB Button (blood type)', # Ab Button (Blood Type) (0x1f18e,): 'AB Button (blood type)', # Ab Button (Blood Type)
(0x1f191,): 'CL Button', # Cl Button (0x1f191,): 'CL Button', # Cl Button
(0x1f192,): 'COOL Button', # Cool Button (0x1f192,): 'COOL Button', # Cool Button
(0x1f193,): 'FREE Button', # Free Button (0x1f193,): 'FREE Button', # Free Button
(0x1f194,): 'ID Button', # Id Button (0x1f194,): 'ID Button', # Id Button
(0x1f195,): 'NEW Button', # New Button (0x1f195,): 'NEW Button', # New Button
(0x1f196,): 'NG Button', # Ng Button (0x1f196,): 'NG Button', # Ng Button
(0x1f197,): 'OK Button', # Ok Button (0x1f197,): 'OK Button', # Ok Button
(0x1f198,): 'SOS Button', # Sos Button (0x1f198,): 'SOS Button', # Sos Button
(0x1f199,): 'UP! Button', # Up! Button (0x1f199,): 'UP! Button', # Up! Button
(0x1f19a,): 'VS Button', # Vs Button (0x1f19a,): 'VS Button', # Vs Button
(0x1f3e7,): 'ATM Sign', # Atm Sign (0x1f3e7,): 'ATM Sign', # Atm Sign
(0x1f44C,): 'OK Hand', # Ok Hand (0x1f44C,): 'OK Hand', # Ok Hand
(0x1f452,): 'Womans Hat', (0x1f452,): 'Womans Hat',
(0x1f45a,): 'Womans Clothes', (0x1f45a,): 'Womans Clothes',
(0x1f45e,): 'Mans Shoe', (0x1f45e,): 'Mans Shoe',
(0x1f461,): 'Womans Sandal', (0x1f461,): 'Womans Sandal',
(0x1f462,): 'Womans Boot', (0x1f462,): 'Womans Boot',
(0x1f519,): 'BACK Arrow', # Back Arrow (0x1f519,): 'BACK Arrow', # Back Arrow
(0x1f51a,): 'END Arrow', # End Arrow (0x1f51a,): 'END Arrow', # End Arrow
(0x1f51b,): 'ON! Arrow', # On! Arrow (0x1f51b,): 'ON! Arrow', # On! Arrow
(0x1f51c,): 'SOON Arrow', # Soon Arrow (0x1f51c,): 'SOON Arrow', # Soon Arrow
(0x1f51d,): 'TOP Arrow', # Top Arrow (0x1f51d,): 'TOP Arrow', # Top Arrow
(0x1f6b9,): 'Mens Room', (0x1f6b9,): 'Mens Room',
(0x1f6ba,): 'Womens Room', (0x1f6ba,): 'Womens Room',
} }
# For the custom sequences we ignore ZWJ, the emoji variation selector # For the custom sequences we ignore ZWJ, the emoji variation selector
# and skin tone modifiers. We can't always ignore gender because # and skin tone modifiers. We can't always ignore gender because
# the gendered sequences match against them, but we ignore gender in other # the gendered sequences match against them, but we ignore gender in other
# cases so we define a separate set of gendered emoji to remove. # cases so we define a separate set of gendered emoji to remove.
_NON_GENDER_CPS_TO_STRIP = frozenset( _NON_GENDER_CPS_TO_STRIP = frozenset(
[0xfe0f, 0x200d] + [0xfe0f, 0x200d] +
range(unicode_data._FITZ_START, unicode_data._FITZ_END + 1)) range(unicode_data._FITZ_START, unicode_data._FITZ_END + 1))
_GENDER_CPS_TO_STRIP = frozenset([0x2640, 0x2642, 0x1f468, 0x1f469]) _GENDER_CPS_TO_STRIP = frozenset([0x2640, 0x2642, 0x1f468, 0x1f469])
def _custom_name(seq): def _custom_name(seq):
"""Apply three kinds of custom names, based on the sequence.""" """Apply three kinds of custom names, based on the sequence."""
seq = tuple([cp for cp in seq if cp not in _NON_GENDER_CPS_TO_STRIP]) seq = tuple([cp for cp in seq if cp not in _NON_GENDER_CPS_TO_STRIP])
name = _CUSTOM_CAPS_NAMES.get(seq) name = _CUSTOM_CAPS_NAMES.get(seq)
if name: if name:
return name return name
# Single characters that participate in sequences (e.g. fire truck in the # Single characters that participate in sequences (e.g. fire truck in the
# firefighter sequences) should not get converted. Single characters # firefighter sequences) should not get converted. Single characters
# are in the custom caps names set but not the other sets. # are in the custom caps names set but not the other sets.
if len(seq) == 1: if len(seq) == 1:
return None return None
name = _CUSTOM_GENDERED_SEQ_NAMES.get(seq) name = _CUSTOM_GENDERED_SEQ_NAMES.get(seq)
if name: if name:
return name return name
seq = tuple([cp for cp in seq if cp not in _GENDER_CPS_TO_STRIP]) seq = tuple([cp for cp in seq if cp not in _GENDER_CPS_TO_STRIP])
name = _CUSTOM_SEQ_NAMES.get(seq) name = _CUSTOM_SEQ_NAMES.get(seq)
return name return name
def _standard_name(seq): def _standard_name(seq):
"""Use the standard emoji name, with some algorithmic modifications. """Use the standard emoji name, with some algorithmic modifications.
We want to ignore skin-tone modifiers (but of course if the sequence _is_ We want to ignore skin-tone modifiers (but of course if the sequence _is_
the skin-tone modifier itself we keep that). So we strip these so we can the skin-tone modifier itself we keep that). So we strip these so we can
start with the generic name ignoring skin tone. start with the generic name ignoring skin tone.
Non-emoji that are turned into emoji using the emoji VS have '(emoji) ' Non-emoji that are turned into emoji using the emoji VS have '(emoji) '
prepended to them, so strip that. prepended to them, so strip that.
Regional indicator symbol names are a bit long, so shorten them. Regional indicator symbol names are a bit long, so shorten them.
Regional sequences are assumed to be ok as-is in terms of capitalization and Regional sequences are assumed to be ok as-is in terms of capitalization and
punctuation, so no modifications are applied to them. punctuation, so no modifications are applied to them.
After title-casing we make some English articles/prepositions lower-case After title-casing we make some English articles/prepositions lower-case
again. We also replace '&' with 'and'; Unicode seems rather fond of again. We also replace '&' with 'and'; Unicode seems rather fond of
ampersand.""" ampersand."""
if not unicode_data.is_skintone_modifier(seq[0]): if not unicode_data.is_skintone_modifier(seq[0]):
seq = tuple([cp for cp in seq if not unicode_data.is_skintone_modifier(cp)]) seq = tuple([cp for cp in seq if not unicode_data.is_skintone_modifier(cp)])
name = unicode_data.get_emoji_sequence_name(seq) name = unicode_data.get_emoji_sequence_name(seq)
if name.startswith('(emoji) '): if name.startswith('(emoji) '):
name = name[8:] name = name[8:]
if len(seq) == 1 and unicode_data.is_regional_indicator(seq[0]): if len(seq) == 1 and unicode_data.is_regional_indicator(seq[0]):
return 'Regional Symbol ' + unicode_data.regional_indicator_to_ascii(seq[0]) return 'Regional Symbol ' + unicode_data.regional_indicator_to_ascii(seq[0])
if (unicode_data.is_regional_indicator_seq(seq) or if (unicode_data.is_regional_indicator_seq(seq) or
unicode_data.is_regional_tag_seq(seq)): unicode_data.is_regional_tag_seq(seq)):
return name return name
name = name.title() name = name.title()
# Require space delimiting just in case... # Require space delimiting just in case...
name = re.sub(r'\s&\s', ' and ', name) name = re.sub(r'\s&\s', ' and ', name)
name = re.sub( name = re.sub(
# not \b at start because we retain capital at start of phrase # not \b at start because we retain capital at start of phrase
r'(\s(:?A|And|From|In|Of|With|For))\b', lambda s: s.group(1).lower(), r'(\s(:?A|And|From|In|Of|With|For))\b', lambda s: s.group(1).lower(),
name) name)
return name return name
def _name_data(seq, seq_file): def _name_data(seq, seq_file):
name = _custom_name(seq) or _standard_name(seq) name = _custom_name(seq) or _standard_name(seq)
# we don't need canonical sequences # we don't need canonical sequences
sequence = ''.join('&#x%x;' % cp for cp in seq if cp != 0xfe0f) sequence = ''.join('&#x%x;' % cp for cp in seq if cp != 0xfe0f)
fname = path.basename(seq_file) fname = path.basename(seq_file)
return fname, sequence, name return fname, sequence, name
def generate_names( def generate_names(
src_dir, dst_dir, skip_limit=20, omit_groups=None, pretty_print=False, src_dir, dst_dir, skip_limit=20, omit_groups=None, pretty_print=False,
verbose=False): verbose=False):
srcdir = tool_utils.resolve_path(src_dir) srcdir = tool_utils.resolve_path(src_dir)
if not path.isdir(srcdir): if not path.isdir(srcdir):
print('%s is not a directory' % src_dir, file=sys.stderr) print('%s is not a directory' % src_dir, file=sys.stderr)
return return
if omit_groups: if omit_groups:
unknown_groups = set(omit_groups) - set(unicode_data.get_emoji_groups()) unknown_groups = set(omit_groups) - set(unicode_data.get_emoji_groups())
if unknown_groups: if unknown_groups:
print('did not recognize %d group%s: %s' % ( print('did not recognize %d group%s: %s' % (
len(unknown_groups), '' if len(unknown_groups) == 1 else 's', len(unknown_groups), '' if len(unknown_groups) == 1 else 's',
', '.join('"%s"' % g for g in omit_groups if g in unknown_groups)), file=sys.stderr) ', '.join('"%s"' % g for g in omit_groups if g in unknown_groups)), file=sys.stderr)
print('valid groups are:\n %s' % ( print('valid groups are:\n %s' % (
'\n '.join(g for g in unicode_data.get_emoji_groups())), file=sys.stderr) '\n '.join(g for g in unicode_data.get_emoji_groups())), file=sys.stderr)
return return
print('omitting %d group%s: %s' % ( print('omitting %d group%s: %s' % (
len(omit_groups), '' if len(omit_groups) == 1 else 's', len(omit_groups), '' if len(omit_groups) == 1 else 's',
', '.join('"%s"' % g for g in omit_groups))) ', '.join('"%s"' % g for g in omit_groups)))
else: else:
# might be None # might be None
print('keeping all groups') print('keeping all groups')
omit_groups = [] omit_groups = []
# make sure the destination exists # make sure the destination exists
dstdir = tool_utils.ensure_dir_exists( dstdir = tool_utils.ensure_dir_exists(
tool_utils.resolve_path(dst_dir)) tool_utils.resolve_path(dst_dir))
# _get_image_data returns canonical cp sequences # _get_image_data returns canonical cp sequences
print('src dir:', srcdir) print('src dir:', srcdir)
seq_to_file = generate_emoji_html._get_image_data(srcdir, 'png', 'emoji_u') seq_to_file = generate_emoji_html._get_image_data(srcdir, 'png', 'emoji_u')
print('seq to file has %d sequences' % len(seq_to_file)) print('seq to file has %d sequences' % len(seq_to_file))
# Aliases add non-gendered versions using gendered images for the most part. # Aliases add non-gendered versions using gendered images for the most part.
# But when we display the images, we don't distinguish genders in the # But when we display the images, we don't distinguish genders in the
# naming, we rely on the images-- so these look redundant. So we # naming, we rely on the images-- so these look redundant. So we
# intentionally don't generate images for these. # intentionally don't generate images for these.
# However, the alias file also includes the flag aliases, which we do want, # However, the alias file also includes the flag aliases, which we do want,
# and it also fails to exclude the unknown flag pua (since it doesn't # and it also fails to exclude the unknown flag pua (since it doesn't
# map to anything), so we need to adjust for this. # map to anything), so we need to adjust for this.
canonical_aliases = generate_emoji_html._get_canonical_aliases() canonical_aliases = generate_emoji_html._get_canonical_aliases()
aliases = set([ aliases = set([
cps for cps in canonical_aliases.keys() cps for cps in canonical_aliases.keys()
if not unicode_data.is_regional_indicator_seq(cps)]) if not unicode_data.is_regional_indicator_seq(cps)])
aliases.add((0xfe82b,)) # unknown flag PUA aliases.add((0xfe82b,)) # unknown flag PUA
excluded = aliases | generate_emoji_html._get_canonical_excluded() excluded = aliases | generate_emoji_html._get_canonical_excluded()
# The flag aliases have distinct names, so we _do_ want to show them # The flag aliases have distinct names, so we _do_ want to show them
# multiple times. # multiple times.
to_add = {} to_add = {}
for seq in canonical_aliases: for seq in canonical_aliases:
if unicode_data.is_regional_indicator_seq(seq): if unicode_data.is_regional_indicator_seq(seq):
replace_seq = canonical_aliases[seq] replace_seq = canonical_aliases[seq]
if seq in seq_to_file: if seq in seq_to_file:
print('warning, alias %s has file %s' % ( print('warning, alias %s has file %s' % (
unicode_data.regional_indicator_seq_to_string(seq), unicode_data.regional_indicator_seq_to_string(seq),
seq_to_file[seq])) seq_to_file[seq]))
continue continue
replace_file = seq_to_file.get(replace_seq) replace_file = seq_to_file.get(replace_seq)
if replace_file: if replace_file:
to_add[seq] = replace_file to_add[seq] = replace_file
seq_to_file.update(to_add) seq_to_file.update(to_add)
data = [] data = []
last_skipped_group = None last_skipped_group = None
skipcount = 0 skipcount = 0
for group in unicode_data.get_emoji_groups(): for group in unicode_data.get_emoji_groups():
if group in omit_groups: if group in omit_groups:
continue continue
name_data = [] name_data = []
for seq in unicode_data.get_emoji_in_group(group): for seq in unicode_data.get_emoji_in_group(group):
if seq in excluded: if seq in excluded:
continue continue
seq_file = seq_to_file.get(seq, None) seq_file = seq_to_file.get(seq, None)
if seq_file is None: if seq_file is None:
skipcount += 1 skipcount += 1
if verbose: if verbose:
if group != last_skipped_group: if group != last_skipped_group:
print('group %s' % group) print('group %s' % group)
last_skipped_group = group last_skipped_group = group
print(' %s (%s)' % ( print(' %s (%s)' % (
unicode_data.seq_to_string(seq), unicode_data.seq_to_string(seq),
', '.join(unicode_data.name(cp, 'x') for cp in seq))) ', '.join(unicode_data.name(cp, 'x') for cp in seq)))
if skip_limit >= 0 and skipcount > skip_limit: if skip_limit >= 0 and skipcount > skip_limit:
raise Exception('skipped too many items') raise Exception('skipped too many items')
else: else:
name_data.append(_name_data(seq, seq_file)) name_data.append(_name_data(seq, seq_file))
data.append({'category': group, 'emojis': name_data}) data.append({'category': group, 'emojis': name_data})
outfile = path.join(dstdir, 'data.json') outfile = path.join(dstdir, 'data.json')
with open(outfile, 'w') as f: with open(outfile, 'w') as f:
indent = 2 if pretty_print else None indent = 2 if pretty_print else None
separators = None if pretty_print else (',', ':') separators = None if pretty_print else (',', ':')
json.dump(data, f, indent=indent, separators=separators) json.dump(data, f, indent=indent, separators=separators)
print('wrote %s' % outfile) print('wrote %s' % outfile)
def main(): def main():
DEFAULT_DSTDIR = '[emoji]/emoji' DEFAULT_DSTDIR = '[emoji]/emoji'
DEFAULT_IMAGEDIR = '[emoji]/build/compressed_pngs' DEFAULT_IMAGEDIR = '[emoji]/build/compressed_pngs'
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-s', '--srcdir', help='directory containing images (default %s)' % '-s', '--srcdir', help='directory containing images (default %s)' %
DEFAULT_IMAGEDIR, metavar='dir', default=DEFAULT_IMAGEDIR) DEFAULT_IMAGEDIR, metavar='dir', default=DEFAULT_IMAGEDIR)
parser.add_argument( parser.add_argument(
'-d', '--dstdir', help='name of destination directory (default %s)' % '-d', '--dstdir', help='name of destination directory (default %s)' %
DEFAULT_DSTDIR, metavar='fname', default=DEFAULT_DSTDIR) DEFAULT_DSTDIR, metavar='fname', default=DEFAULT_DSTDIR)
parser.add_argument( parser.add_argument(
'-p', '--pretty_print', help='pretty-print json file', '-p', '--pretty_print', help='pretty-print json file',
action='store_true') action='store_true')
parser.add_argument( parser.add_argument(
'-m', '--missing_limit', help='number of missing images before failure ' '-m', '--missing_limit', help='number of missing images before failure '
'(default 20), use -1 for no limit', metavar='n', default=20) '(default 20), use -1 for no limit', metavar='n', default=20)
parser.add_argument( parser.add_argument(
'--omit_groups', help='names of groups to omit (default "Misc")', '--omit_groups', help='names of groups to omit (default "Misc, Flags")',
metavar='name', default=['Misc'], nargs='*') metavar='name', default=['Misc', 'Flags'], nargs='*')
parser.add_argument( parser.add_argument(
'-v', '--verbose', help='print progress information to stdout', '-v', '--verbose', help='print progress information to stdout',
action='store_true') action='store_true')
args = parser.parse_args() args = parser.parse_args()
generate_names( generate_names(
args.srcdir, args.dstdir, args.missing_limit, args.omit_groups, args.srcdir, args.dstdir, args.missing_limit, args.omit_groups,
pretty_print=args.pretty_print, verbose=args.verbose) pretty_print=args.pretty_print, verbose=args.verbose)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -1,96 +1,96 @@
from __future__ import print_function from __future__ import print_function
import os import os
from os import path from os import path
import subprocess import subprocess
OUTPUT_DIR = '/tmp/placeholder_emoji' OUTPUT_DIR = '/tmp/placeholder_emoji'
def generate_image(name, text): def generate_image(name, text):
print(name, text.replace('\n', '_')) print(name, text.replace('\n', '_'))
subprocess.check_call( subprocess.check_call(
['convert', '-size', '100x100', 'label:%s' % text, ['convert', '-size', '100x100', 'label:%s' % text,
'%s/%s' % (OUTPUT_DIR, name)]) '%s/%s' % (OUTPUT_DIR, name)])
def is_color_patch(cp): def is_color_patch(cp):
return cp >= 0x1f3fb and cp <= 0x1f3ff return cp >= 0x1f3fb and cp <= 0x1f3ff
def has_color_patch(values): def has_color_patch(values):
for v in values: for v in values:
if is_color_patch(v): if is_color_patch(v):
return True return True
return False return False
def regional_to_ascii(cp): def regional_to_ascii(cp):
return unichr(ord('A') + cp - 0x1f1e6) return unichr(ord('A') + cp - 0x1f1e6)
def is_flag_sequence(values): def is_flag_sequence(values):
if len(values) != 2: if len(values) != 2:
return False return False
for v in values: for v in values:
v -= 0x1f1e6 v -= 0x1f1e6
if v < 0 or v > 25: if v < 0 or v > 25:
return False return False
return True return True
def is_keycap_sequence(values): def is_keycap_sequence(values):
return len(values) == 2 and values[1] == 0x20e3 return len(values) == 2 and values[1] == 0x20e3
def get_keycap_text(values): def get_keycap_text(values):
return '-%c-' % unichr(values[0]) # convert gags on '[' return '-%c-' % unichr(values[0]) # convert gags on '['
char_map = { char_map = {
0x1f468: 'M', 0x1f468: 'M',
0x1f469: 'W', 0x1f469: 'W',
0x1f466: 'B', 0x1f466: 'B',
0x1f467: 'G', 0x1f467: 'G',
0x2764: 'H', # heavy black heart, no var sel 0x2764: 'H', # heavy black heart, no var sel
0x1f48b: 'K', # kiss mark 0x1f48b: 'K', # kiss mark
0x200D: '-', # zwj placeholder 0x200D: '-', # zwj placeholder
0xfe0f: '-', # variation selector placeholder 0xfe0f: '-', # variation selector placeholder
0x1f441: 'I', # Eye 0x1f441: 'I', # Eye
0x1f5e8: 'W', # 'witness' (left speech bubble) 0x1f5e8: 'W', # 'witness' (left speech bubble)
} }
def get_combining_text(values): def get_combining_text(values):
chars = [] chars = []
for v in values: for v in values:
char = char_map.get(v, None) char = char_map.get(v, None)
if not char: if not char:
return None return None
if char != '-': if char != '-':
chars.append(char) chars.append(char)
return ''.join(chars) return ''.join(chars)
if not path.isdir(OUTPUT_DIR): if not path.isdir(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR) os.makedirs(OUTPUT_DIR)
with open('sequences.txt', 'r') as f: with open('sequences.txt', 'r') as f:
for seq in f: for seq in f:
seq = seq.strip() seq = seq.strip()
text = None text = None
values = [int(code, 16) for code in seq.split('_')] values = [int(code, 16) for code in seq.split('_')]
if len(values) == 1: if len(values) == 1:
val = values[0] val = values[0]
text = '%04X' % val # ensure upper case format text = '%04X' % val # ensure upper case format
elif is_flag_sequence(values): elif is_flag_sequence(values):
text = ''.join(regional_to_ascii(cp) for cp in values) text = ''.join(regional_to_ascii(cp) for cp in values)
elif has_color_patch(values): elif has_color_patch(values):
print('skipping color patch sequence %s' % seq) print('skipping color patch sequence %s' % seq)
elif is_keycap_sequence(values): elif is_keycap_sequence(values):
text = get_keycap_text(values) text = get_keycap_text(values)
else: else:
text = get_combining_text(values) text = get_combining_text(values)
if not text: if not text:
print('missing %s' % seq) print('missing %s' % seq)
if text: if text:
if len(text) > 3: if len(text) > 3:
if len(text) == 4: if len(text) == 4:
hi = text[:2] hi = text[:2]
lo = text[2:] lo = text[2:]
else: else:
hi = text[:-3] hi = text[:-3]
lo = text[-3:] lo = text[-3:]
text = '%s\n%s' % (hi, lo) text = '%s\n%s' % (hi, lo)
generate_image('emoji_u%s.png' % seq, text) generate_image('emoji_u%s.png' % seq, text)

View file

@ -1,159 +1,159 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright 2017 Google Inc. All rights reserved. # Copyright 2017 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Generate 72x72 thumbnails including aliases. """Generate 72x72 thumbnails including aliases.
Takes a source directory of images named using our emoji filename Takes a source directory of images named using our emoji filename
conventions and writes thumbnails of them into the destination conventions and writes thumbnails of them into the destination
directory. If a file is a target of one or more aliases, creates directory. If a file is a target of one or more aliases, creates
copies named for the aliases.""" copies named for the aliases."""
import argparse import argparse
import collections import collections
import logging import logging
import os import os
from os import path from os import path
import shutil import shutil
import subprocess import subprocess
import add_aliases import add_aliases
from nototools import tool_utils from nototools import tool_utils
from nototools import unicode_data from nototools import unicode_data
logger = logging.getLogger('emoji_thumbnails') logger = logging.getLogger('emoji_thumbnails')
def create_thumbnail(src_path, dst_path, crop): def create_thumbnail(src_path, dst_path, crop):
# Uses imagemagik # Uses imagemagik
# We need images exactly 72x72 in size, with transparent background. # We need images exactly 72x72 in size, with transparent background.
# Remove 4-pixel LR margins from 136x128 source images if we crop. # Remove 4-pixel LR margins from 136x128 source images if we crop.
if crop: if crop:
cmd = [ cmd = [
'convert', src_path, '-crop', '128x128+4+0!', '-thumbnail', '72x72', 'convert', src_path, '-crop', '128x128+4+0!', '-thumbnail', '72x72',
'PNG32:' + dst_path] 'PNG32:' + dst_path]
else: else:
cmd = [ cmd = [
'convert', '-thumbnail', '72x72', '-gravity', 'center', '-background', 'convert', '-thumbnail', '72x72', '-gravity', 'center', '-background',
'none', '-extent', '72x72', src_path, 'PNG32:' + dst_path] 'none', '-extent', '72x72', src_path, 'PNG32:' + dst_path]
subprocess.check_call(cmd) subprocess.check_call(cmd)
def get_inv_aliases(): def get_inv_aliases():
"""Return a mapping from target to list of sources for all alias """Return a mapping from target to list of sources for all alias
targets in either the default alias table or the unknown_flag alias targets in either the default alias table or the unknown_flag alias
table.""" table."""
inv_aliases = collections.defaultdict(list) inv_aliases = collections.defaultdict(list)
standard_aliases = add_aliases.read_default_emoji_aliases() standard_aliases = add_aliases.read_default_emoji_aliases()
for k, v in standard_aliases.iteritems(): for k, v in standard_aliases.iteritems():
inv_aliases[v].append(k) inv_aliases[v].append(k)
unknown_flag_aliases = add_aliases.read_emoji_aliases( unknown_flag_aliases = add_aliases.read_emoji_aliases(
'unknown_flag_aliases.txt') 'unknown_flag_aliases.txt')
for k, v in unknown_flag_aliases.iteritems(): for k, v in unknown_flag_aliases.iteritems():
inv_aliases[v].append(k) inv_aliases[v].append(k)
return inv_aliases return inv_aliases
def filename_to_sequence(filename, prefix, suffix): def filename_to_sequence(filename, prefix, suffix):
if not filename.startswith(prefix) and filename.endswith(suffix): if not filename.startswith(prefix) and filename.endswith(suffix):
raise ValueError('bad prefix or suffix: "%s"' % filename) raise ValueError('bad prefix or suffix: "%s"' % filename)
seq_str = filename[len(prefix): -len(suffix)] seq_str = filename[len(prefix): -len(suffix)]
seq = unicode_data.string_to_seq(seq_str) seq = unicode_data.string_to_seq(seq_str)
if not unicode_data.is_cp_seq(seq): if not unicode_data.is_cp_seq(seq):
raise ValueError('sequence includes non-codepoint: "%s"' % filename) raise ValueError('sequence includes non-codepoint: "%s"' % filename)
return seq return seq
def sequence_to_filename(seq, prefix, suffix): def sequence_to_filename(seq, prefix, suffix):
return ''.join((prefix, unicode_data.seq_to_string(seq), suffix)) return ''.join((prefix, unicode_data.seq_to_string(seq), suffix))
def create_thumbnails_and_aliases(src_dir, dst_dir, crop, dst_prefix): def create_thumbnails_and_aliases(src_dir, dst_dir, crop, dst_prefix):
"""Creates thumbnails in dst_dir based on sources in src.dir, using """Creates thumbnails in dst_dir based on sources in src.dir, using
dst_prefix. Assumes the source prefix is 'emoji_u' and the common suffix dst_prefix. Assumes the source prefix is 'emoji_u' and the common suffix
is '.png'.""" is '.png'."""
src_dir = tool_utils.resolve_path(src_dir) src_dir = tool_utils.resolve_path(src_dir)
if not path.isdir(src_dir): if not path.isdir(src_dir):
raise ValueError('"%s" is not a directory') raise ValueError('"%s" is not a directory')
dst_dir = tool_utils.ensure_dir_exists(tool_utils.resolve_path(dst_dir)) dst_dir = tool_utils.ensure_dir_exists(tool_utils.resolve_path(dst_dir))
src_prefix = 'emoji_u' src_prefix = 'emoji_u'
suffix = '.png' suffix = '.png'
inv_aliases = get_inv_aliases() inv_aliases = get_inv_aliases()
for src_file in os.listdir(src_dir): for src_file in os.listdir(src_dir):
try: try:
seq = unicode_data.strip_emoji_vs( seq = unicode_data.strip_emoji_vs(
filename_to_sequence(src_file, src_prefix, suffix)) filename_to_sequence(src_file, src_prefix, suffix))
except ValueError as ve: except ValueError as ve:
logger.warning('Error (%s), skipping' % ve) logger.warning('Error (%s), skipping' % ve)
continue continue
src_path = path.join(src_dir, src_file) src_path = path.join(src_dir, src_file)
dst_file = sequence_to_filename(seq, dst_prefix, suffix) dst_file = sequence_to_filename(seq, dst_prefix, suffix)
dst_path = path.join(dst_dir, dst_file) dst_path = path.join(dst_dir, dst_file)
create_thumbnail(src_path, dst_path, crop) create_thumbnail(src_path, dst_path, crop)
logger.info('wrote thumbnail%s: %s' % ( logger.info('wrote thumbnail%s: %s' % (
' with crop' if crop else '', dst_file)) ' with crop' if crop else '', dst_file))
for alias_seq in inv_aliases.get(seq, ()): for alias_seq in inv_aliases.get(seq, ()):
alias_file = sequence_to_filename(alias_seq, dst_prefix, suffix) alias_file = sequence_to_filename(alias_seq, dst_prefix, suffix)
alias_path = path.join(dst_dir, alias_file) alias_path = path.join(dst_dir, alias_file)
shutil.copy2(dst_path, alias_path) shutil.copy2(dst_path, alias_path)
logger.info('wrote alias: %s' % alias_file) logger.info('wrote alias: %s' % alias_file)
def main(): def main():
SRC_DEFAULT = '[emoji]/build/compressed_pngs' SRC_DEFAULT = '[emoji]/build/compressed_pngs'
PREFIX_DEFAULT = 'android_' PREFIX_DEFAULT = 'android_'
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-s', '--src_dir', help='source images (default \'%s\')' % SRC_DEFAULT, '-s', '--src_dir', help='source images (default \'%s\')' % SRC_DEFAULT,
default=SRC_DEFAULT, metavar='dir') default=SRC_DEFAULT, metavar='dir')
parser.add_argument( parser.add_argument(
'-d', '--dst_dir', help='destination directory', metavar='dir', '-d', '--dst_dir', help='destination directory', metavar='dir',
required=True) required=True)
parser.add_argument( parser.add_argument(
'-p', '--prefix', help='prefix for thumbnail (default \'%s\')' % '-p', '--prefix', help='prefix for thumbnail (default \'%s\')' %
PREFIX_DEFAULT, default=PREFIX_DEFAULT, metavar='str') PREFIX_DEFAULT, default=PREFIX_DEFAULT, metavar='str')
parser.add_argument( parser.add_argument(
'-c', '--crop', help='crop images (will automatically crop if ' '-c', '--crop', help='crop images (will automatically crop if '
'src dir is the default)', action='store_true') 'src dir is the default)', action='store_true')
parser.add_argument( parser.add_argument(
'-v', '--verbose', help='write log output', metavar='level', '-v', '--verbose', help='write log output', metavar='level',
choices='warning info debug'.split(), const='info', choices='warning info debug'.split(), const='info',
nargs='?') nargs='?')
args = parser.parse_args() args = parser.parse_args()
if args.verbose is not None: if args.verbose is not None:
logging.basicConfig(level=getattr(logging, args.verbose.upper())) logging.basicConfig(level=getattr(logging, args.verbose.upper()))
crop = args.crop or (args.src_dir == SRC_DEFAULT) crop = args.crop or (args.src_dir == SRC_DEFAULT)
create_thumbnails_and_aliases( create_thumbnails_and_aliases(
args.src_dir, args.dst_dir, crop, args.prefix) args.src_dir, args.dst_dir, crop, args.prefix)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,202 +1,202 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright 2015 Google, Inc. All Rights Reserved. # Copyright 2015 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Doug Felt # Google Author(s): Doug Felt
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import os import os
import os.path import os.path
import re import re
import sys import sys
from fontTools import ttx from fontTools import ttx
import add_svg_glyphs import add_svg_glyphs
def do_generate_test_html(font_basename, pairs, glyph=None, verbosity=1): def do_generate_test_html(font_basename, pairs, glyph=None, verbosity=1):
header = r"""<!DOCTYPE html> header = r"""<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style type="text/css"> <style type="text/css">
@font-face { font-family: svgfont; src: url("%s") } @font-face { font-family: svgfont; src: url("%s") }
body { font-family: sans-serif; font-size: 24px } body { font-family: sans-serif; font-size: 24px }
#emoji span { font-family: svgfont, sans-serif } #emoji span { font-family: svgfont, sans-serif }
#panel { font-family: svgfont, sans-serif; font-size: 256px } #panel { font-family: svgfont, sans-serif; font-size: 256px }
#paneltitle { font-family: sans-serif; font-size: 36px } #paneltitle { font-family: sans-serif; font-size: 36px }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
function hexify(text) { function hexify(text) {
var surr_offset = 0x10000 - (0xd800 << 10) - 0xdc00 var surr_offset = 0x10000 - (0xd800 << 10) - 0xdc00
var str = text.trim() var str = text.trim()
var len = str.length var len = str.length
var result = "" var result = ""
for (var i = 0; i < len; ++i) { for (var i = 0; i < len; ++i) {
var cp = str.charCodeAt(i) var cp = str.charCodeAt(i)
if (cp >= 0xd800 && cp < 0xdc00 && i < len - 1) { if (cp >= 0xd800 && cp < 0xdc00 && i < len - 1) {
ncp = str.charCodeAt(i+1) ncp = str.charCodeAt(i+1)
if (ncp >= 0xdc00 && ncp < 0xe000) { if (ncp >= 0xdc00 && ncp < 0xe000) {
cp = (cp << 10) + ncp + surr_offset cp = (cp << 10) + ncp + surr_offset
++i; ++i;
} }
} }
result += " 0x" + cp.toString(16) result += " 0x" + cp.toString(16)
} }
return result return result
}; };
function showText(event) { function showText(event) {
var text = event.target.textContent var text = event.target.textContent
var p = document.getElementById('panel') var p = document.getElementById('panel')
p.textContent = text p.textContent = text
p = document.getElementById('paneltitle') p = document.getElementById('paneltitle')
p.textContent = hexify(text) p.textContent = hexify(text)
}; };
function setup() { function setup() {
var t = document.getElementById('emoji') var t = document.getElementById('emoji')
var tdlist = t.getElementsByTagName('span') var tdlist = t.getElementsByTagName('span')
for (var i = 0; i < tdlist.length; ++i) { for (var i = 0; i < tdlist.length; ++i) {
var e = tdlist[i] var e = tdlist[i]
e.onmouseover = showText e.onmouseover = showText
} }
}; };
</script> </script>
</head>""" </head>"""
body_head = r"""<body onload="setup();"> body_head = r"""<body onload="setup();">
<p>Test for SVG glyphs in %(font)s. It uses the proposed <p>Test for SVG glyphs in %(font)s. It uses the proposed
<a href="http://lists.w3.org/Archives/Public/public-svgopentype/2013Jul/0003.html">SVG-in-OpenType format</a>. <a href="http://lists.w3.org/Archives/Public/public-svgopentype/2013Jul/0003.html">SVG-in-OpenType format</a>.
View using Firefox&nbsp;26 and later. View using Firefox&nbsp;26 and later.
<div style="float:left; text-align:center; margin:0 10px; width:40%%"> <div style="float:left; text-align:center; margin:0 10px; width:40%%">
<div id='panel' style="margin-left:auto; margin-right:auto">%(glyph)s</div> <div id='panel' style="margin-left:auto; margin-right:auto">%(glyph)s</div>
<div id='paneltitle' style="margin-left:auto; margin-right:auto">%(glyph_hex)s</div> <div id='paneltitle' style="margin-left:auto; margin-right:auto">%(glyph_hex)s</div>
</div> </div>
<div id='emoji'><p>""" <div id='emoji'><p>"""
body_tail = r"""</div> body_tail = r"""</div>
</body> </body>
</html> </html>
""" """
font_name = font_basename + ".woff" font_name = font_basename + ".woff"
html_name = font_basename + "_test.html" html_name = font_basename + "_test.html"
found_initial_glyph = False found_initial_glyph = False
initial_glyph_str = None; initial_glyph_str = None;
initial_glyph_hex = None; initial_glyph_hex = None;
text_parts = [] text_parts = []
for glyphstr, _ in pairs: for glyphstr, _ in pairs:
name_parts = [] name_parts = []
hex_parts = [] hex_parts = []
for cp in glyphstr: for cp in glyphstr:
hex_str = hex(ord(cp)) hex_str = hex(ord(cp))
name_parts.append('&#x%s;' % hex_str[2:]) name_parts.append('&#x%s;' % hex_str[2:])
hex_parts.append(hex_str) hex_parts.append(hex_str)
glyph_str = ''.join(name_parts) glyph_str = ''.join(name_parts)
if not found_initial_glyph: if not found_initial_glyph:
if not glyph or glyph_str == glyph: if not glyph or glyph_str == glyph:
initial_glyph_str = glyph_str initial_glyph_str = glyph_str
initial_glyph_hex = ' '.join(hex_parts) initial_glyph_hex = ' '.join(hex_parts)
found_initial_glyph = True found_initial_glyph = True
elif not initial_glyph_str: elif not initial_glyph_str:
initial_glyph_str = glyph_str initial_glyph_str = glyph_str
initial_glyph_hex = ' '.join(hex_parts) initial_glyph_hex = ' '.join(hex_parts)
text = '<span>%s</span>' % glyph_str text = '<span>%s</span>' % glyph_str
text_parts.append(text) text_parts.append(text)
if verbosity and glyph and not found_initial_glyph: if verbosity and glyph and not found_initial_glyph:
print("Did not find glyph '%s', using initial glyph '%s'" % (glyph, initial_glyph_str)) print("Did not find glyph '%s', using initial glyph '%s'" % (glyph, initial_glyph_str))
elif verbosity > 1 and not glyph: elif verbosity > 1 and not glyph:
print("Using initial glyph '%s'" % initial_glyph_str) print("Using initial glyph '%s'" % initial_glyph_str)
lines = [header % font_name] lines = [header % font_name]
lines.append(body_head % {'font':font_name, 'glyph':initial_glyph_str, lines.append(body_head % {'font':font_name, 'glyph':initial_glyph_str,
'glyph_hex':initial_glyph_hex}) 'glyph_hex':initial_glyph_hex})
lines.extend(text_parts) # we'll end up with space between each emoji lines.extend(text_parts) # we'll end up with space between each emoji
lines.append(body_tail) lines.append(body_tail)
output = '\n'.join(lines) output = '\n'.join(lines)
with open(html_name, 'w') as fp: with open(html_name, 'w') as fp:
fp.write(output) fp.write(output)
if verbosity: if verbosity:
print('Wrote ' + html_name) print('Wrote ' + html_name)
def do_generate_fonts(template_file, font_basename, pairs, reuse=0, verbosity=1): def do_generate_fonts(template_file, font_basename, pairs, reuse=0, verbosity=1):
out_woff = font_basename + '.woff' out_woff = font_basename + '.woff'
if reuse > 1 and os.path.isfile(out_woff) and os.access(out_woff, os.R_OK): if reuse > 1 and os.path.isfile(out_woff) and os.access(out_woff, os.R_OK):
if verbosity: if verbosity:
print('Reusing ' + out_woff) print('Reusing ' + out_woff)
return return
out_ttx = font_basename + '.ttx' out_ttx = font_basename + '.ttx'
if reuse == 0: if reuse == 0:
add_svg_glyphs.add_image_glyphs(template_file, out_ttx, pairs, verbosity=verbosity) add_svg_glyphs.add_image_glyphs(template_file, out_ttx, pairs, verbosity=verbosity)
elif verbosity: elif verbosity:
print('Reusing ' + out_ttx) print('Reusing ' + out_ttx)
quiet=verbosity < 2 quiet=verbosity < 2
font = ttx.TTFont(flavor='woff', quiet=quiet) font = ttx.TTFont(flavor='woff', quiet=quiet)
font.importXML(out_ttx, quiet=quiet) font.importXML(out_ttx, quiet=quiet)
font.save(out_woff) font.save(out_woff)
if verbosity: if verbosity:
print('Wrote ' + out_woff) print('Wrote ' + out_woff)
def main(argv): def main(argv):
usage = """This will search for files that have image_prefix followed by one or more usage = """This will search for files that have image_prefix followed by one or more
hex numbers (separated by underscore if more than one), and end in ".svg". hex numbers (separated by underscore if more than one), and end in ".svg".
For example, if image_prefix is "icons/u", then files with names like For example, if image_prefix is "icons/u", then files with names like
"icons/u1F4A9.svg" or "icons/u1F1EF_1F1F5.svg" will be found. It generates "icons/u1F4A9.svg" or "icons/u1F1EF_1F1F5.svg" will be found. It generates
an SVG font from this, converts it to woff, and also generates an html test an SVG font from this, converts it to woff, and also generates an html test
page containing text for all the SVG glyphs.""" page containing text for all the SVG glyphs."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate font and html test file.', epilog=usage) description='Generate font and html test file.', epilog=usage)
parser.add_argument('template_file', help='name of template .ttx file') parser.add_argument('template_file', help='name of template .ttx file')
parser.add_argument('image_prefix', help='location and prefix of image files') parser.add_argument('image_prefix', help='location and prefix of image files')
parser.add_argument('-i', '--include', help='include files whoses name matches this regex') parser.add_argument('-i', '--include', help='include files whoses name matches this regex')
parser.add_argument('-e', '--exclude', help='exclude files whose name matches this regex') parser.add_argument('-e', '--exclude', help='exclude files whose name matches this regex')
parser.add_argument('-o', '--out_basename', help='base name of (ttx, woff, html) files to generate, ' parser.add_argument('-o', '--out_basename', help='base name of (ttx, woff, html) files to generate, '
'defaults to the template base name') 'defaults to the template base name')
parser.add_argument('-g', '--glyph', help='set the initial glyph text (html encoded string), ' parser.add_argument('-g', '--glyph', help='set the initial glyph text (html encoded string), '
'defaults to first glyph') 'defaults to first glyph')
parser.add_argument('-rt', '--reuse_ttx_font', dest='reuse_font', help='use existing ttx font', parser.add_argument('-rt', '--reuse_ttx_font', dest='reuse_font', help='use existing ttx font',
default=0, const=1, action='store_const') default=0, const=1, action='store_const')
parser.add_argument('-r', '--reuse_font', dest='reuse_font', help='use existing woff font', parser.add_argument('-r', '--reuse_font', dest='reuse_font', help='use existing woff font',
const=2, action='store_const') const=2, action='store_const')
parser.add_argument('-q', '--quiet', dest='v', help='quiet operation', default=1, parser.add_argument('-q', '--quiet', dest='v', help='quiet operation', default=1,
action='store_const', const=0) action='store_const', const=0)
parser.add_argument('-v', '--verbose', dest='v', help='verbose operation', parser.add_argument('-v', '--verbose', dest='v', help='verbose operation',
action='store_const', const=2) action='store_const', const=2)
args = parser.parse_args(argv) args = parser.parse_args(argv)
pairs = add_svg_glyphs.collect_glyphstr_file_pairs( pairs = add_svg_glyphs.collect_glyphstr_file_pairs(
args.image_prefix, 'svg', include=args.include, exclude=args.exclude, verbosity=args.v) args.image_prefix, 'svg', include=args.include, exclude=args.exclude, verbosity=args.v)
add_svg_glyphs.sort_glyphstr_tuples(pairs) add_svg_glyphs.sort_glyphstr_tuples(pairs)
out_basename = args.out_basename out_basename = args.out_basename
if not out_basename: if not out_basename:
out_basename = args.template_file.split('.')[0] # exclude e.g. '.tmpl.ttx' out_basename = args.template_file.split('.')[0] # exclude e.g. '.tmpl.ttx'
if args.v: if args.v:
print("Output basename is %s." % out_basename) print("Output basename is %s." % out_basename)
do_generate_fonts(args.template_file, out_basename, pairs, reuse=args.reuse_font, verbosity=args.v) do_generate_fonts(args.template_file, out_basename, pairs, reuse=args.reuse_font, verbosity=args.v)
do_generate_test_html(out_basename, pairs, glyph=args.glyph, verbosity=args.v) do_generate_test_html(out_basename, pairs, glyph=args.glyph, verbosity=args.v)
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv[1:]) main(sys.argv[1:])

View file

@ -1,72 +1,74 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2014 Google Inc. All rights reserved. # Copyright 2014 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Modify an emoji font to map legacy PUA characters to standard ligatures.""" """Modify an emoji font to map legacy PUA characters to standard ligatures."""
__author__ = 'roozbeh@google.com (Roozbeh Pournader)' __author__ = 'roozbeh@google.com (Roozbeh Pournader)'
import sys import sys
import itertools
from fontTools import ttLib
from fontTools import ttLib
from nototools import font_data
from nototools import font_data
import add_emoji_gsub
import add_emoji_gsub
def get_glyph_name_from_gsub(char_seq, font):
"""Find the glyph name for ligature of a given character sequence from GSUB. def get_glyph_name_from_gsub(char_seq, font):
""" """Find the glyph name for ligature of a given character sequence from GSUB.
cmap = font_data.get_cmap(font) """
# FIXME: So many assumptions are made here. cmap = font_data.get_cmap(font)
try: # FIXME: So many assumptions are made here.
first_glyph = cmap[char_seq[0]] try:
rest_of_glyphs = [cmap[ch] for ch in char_seq[1:]] first_glyph = cmap[char_seq[0]]
except KeyError: rest_of_glyphs = [cmap[ch] for ch in char_seq[1:]]
return None except KeyError:
return None
for lookup in font['GSUB'].table.LookupList.Lookup:
ligatures = lookup.SubTable[0].ligatures for lookup in font['GSUB'].table.LookupList.Lookup:
try: ligatures = lookup.SubTable[0].ligatures
for ligature in ligatures[first_glyph]: try:
if ligature.Component == rest_of_glyphs: for ligature in ligatures[first_glyph]:
return ligature.LigGlyph if ligature.Component == rest_of_glyphs:
except KeyError: return ligature.LigGlyph
continue except KeyError:
return None continue
return None
def add_pua_cmap(source_file, target_file):
"""Add PUA characters to the cmap of the first font and save as second.""" def add_pua_cmap(source_file, target_file):
font = ttLib.TTFont(source_file) """Add PUA characters to the cmap of the first font and save as second."""
cmap = font_data.get_cmap(font) font = ttLib.TTFont(source_file)
for pua, (ch1, ch2) in (add_emoji_gsub.EMOJI_KEYCAPS.items() cmap = font_data.get_cmap(font)
+ add_emoji_gsub.EMOJI_FLAGS.items()): for pua, (ch1, ch2) in itertools.chain(
if pua not in cmap: add_emoji_gsub.EMOJI_KEYCAPS.items(), add_emoji_gsub.EMOJI_FLAGS.items()
glyph_name = get_glyph_name_from_gsub([ch1, ch2], font) ):
if glyph_name is not None: if pua not in cmap:
cmap[pua] = glyph_name glyph_name = get_glyph_name_from_gsub([ch1, ch2], font)
font.save(target_file) if glyph_name is not None:
cmap[pua] = glyph_name
font.save(target_file)
def main(argv):
"""Save the first font given to the second font."""
add_pua_cmap(argv[1], argv[2]) def main(argv):
"""Save the first font given to the second font."""
add_pua_cmap(argv[1], argv[2])
if __name__ == '__main__':
main(sys.argv)
if __name__ == '__main__':
main(sys.argv)

View file

@ -1,126 +1,126 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2016 Google Inc. All rights reserved. # Copyright 2016 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Create a copy of the emoji images that instantiates aliases, etc. as """Create a copy of the emoji images that instantiates aliases, etc. as
symlinks.""" symlinks."""
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import glob import glob
import os import os
from os import path from os import path
import re import re
import shutil import shutil
from nototools import tool_utils from nototools import tool_utils
# copied from third_party/color_emoji/add_glyphs.py # copied from third_party/color_emoji/add_glyphs.py
EXTRA_SEQUENCES = { EXTRA_SEQUENCES = {
'u1F46A': '1F468_200D_1F469_200D_1F466', # MWB 'u1F46A': '1F468_200D_1F469_200D_1F466', # MWB
'u1F491': '1F469_200D_2764_FE0F_200D_1F468', # WHM 'u1F491': '1F469_200D_2764_FE0F_200D_1F468', # WHM
'u1F48F': '1F469_200D_2764_FE0F_200D_1F48B_200D_1F468', # WHKM 'u1F48F': '1F469_200D_2764_FE0F_200D_1F48B_200D_1F468', # WHKM
} }
# Flag aliases - from: to # Flag aliases - from: to
FLAG_ALIASES = { FLAG_ALIASES = {
'BV': 'NO', 'BV': 'NO',
'CP': 'FR', 'CP': 'FR',
'HM': 'AU', 'HM': 'AU',
'SJ': 'NO', 'SJ': 'NO',
'UM': 'US', 'UM': 'US',
} }
OMITTED_FLAGS = set( OMITTED_FLAGS = set(
'BL BQ DG EA EH FK GF GP GS MF MQ NC PM RE TF WF XK YT'.split()) 'BL BQ DG EA EH FK GF GP GS MF MQ NC PM RE TF WF XK YT'.split())
def _flag_str(ris_pair): def _flag_str(ris_pair):
return '_'.join('%04x' % (ord(cp) - ord('A') + 0x1f1e6) return '_'.join('%04x' % (ord(cp) - ord('A') + 0x1f1e6)
for cp in ris_pair) for cp in ris_pair)
def _copy_files(src, dst): def _copy_files(src, dst):
"""Copies files named 'emoji_u*.png' from dst to src, and return a set of """Copies files named 'emoji_u*.png' from dst to src, and return a set of
the names with 'emoji_u' and the extension stripped.""" the names with 'emoji_u' and the extension stripped."""
code_strings = set() code_strings = set()
tool_utils.check_dir_exists(src) tool_utils.check_dir_exists(src)
dst = tool_utils.ensure_dir_exists(dst, clean=True) dst = tool_utils.ensure_dir_exists(dst, clean=True)
for f in glob.glob(path.join(src, 'emoji_u*.png')): for f in glob.glob(path.join(src, 'emoji_u*.png')):
shutil.copy(f, dst) shutil.copy(f, dst)
code_strings.add(path.splitext(path.basename(f))[0][7:]) code_strings.add(path.splitext(path.basename(f))[0][7:])
return code_strings return code_strings
def _alias_people(code_strings, dst): def _alias_people(code_strings, dst):
"""Create aliases for people in dst, based on code_strings.""" """Create aliases for people in dst, based on code_strings."""
for src, ali in sorted(EXTRA_SEQUENCES.items()): for src, ali in sorted(EXTRA_SEQUENCES.items()):
if src[1:].lower() in code_strings: if src[1:].lower() in code_strings:
src_name = 'emoji_%s.png' % src.lower() src_name = 'emoji_%s.png' % src.lower()
ali_name = 'emoji_u%s.png' % ali.lower() ali_name = 'emoji_u%s.png' % ali.lower()
print('creating symlink %s -> %s' % (ali_name, src_name)) print('creating symlink %s -> %s' % (ali_name, src_name))
os.symlink(path.join(dst, src_name), path.join(dst, ali_name)) os.symlink(path.join(dst, src_name), path.join(dst, ali_name))
else: else:
print('people image %s not found' % src, file=os.stderr) print('people image %s not found' % src, file=os.stderr)
def _alias_flags(code_strings, dst): def _alias_flags(code_strings, dst):
for ali, src in sorted(FLAG_ALIASES.items()): for ali, src in sorted(FLAG_ALIASES.items()):
src_str = _flag_str(src) src_str = _flag_str(src)
if src_str in code_strings: if src_str in code_strings:
src_name = 'emoji_u%s.png' % src_str src_name = 'emoji_u%s.png' % src_str
ali_name = 'emoji_u%s.png' % _flag_str(ali) ali_name = 'emoji_u%s.png' % _flag_str(ali)
print('creating symlink %s (%s) -> %s (%s)' % (ali_name, ali, src_name, src)) print('creating symlink %s (%s) -> %s (%s)' % (ali_name, ali, src_name, src))
os.symlink(path.join(dst, src_name), path.join(dst, ali_name)) os.symlink(path.join(dst, src_name), path.join(dst, ali_name))
else: else:
print('flag image %s (%s) not found' % (src_name, src), file=os.stderr) print('flag image %s (%s) not found' % (src_name, src), file=os.stderr)
def _alias_omitted_flags(code_strings, dst): def _alias_omitted_flags(code_strings, dst):
UNKNOWN_FLAG = 'fe82b' UNKNOWN_FLAG = 'fe82b'
if UNKNOWN_FLAG not in code_strings: if UNKNOWN_FLAG not in code_strings:
print('unknown flag missing', file=os.stderr) print('unknown flag missing', file=os.stderr)
return return
dst_name = 'emoji_u%s.png' % UNKNOWN_FLAG dst_name = 'emoji_u%s.png' % UNKNOWN_FLAG
dst_path = path.join(dst, dst_name) dst_path = path.join(dst, dst_name)
for ali in sorted(OMITTED_FLAGS): for ali in sorted(OMITTED_FLAGS):
ali_str = _flag_str(ali) ali_str = _flag_str(ali)
if ali_str in code_strings: if ali_str in code_strings:
print('omitted flag %s has image %s' % (ali, ali_str), file=os.stderr) print('omitted flag %s has image %s' % (ali, ali_str), file=os.stderr)
continue continue
ali_name = 'emoji_u%s.png' % ali_str ali_name = 'emoji_u%s.png' % ali_str
print('creating symlink %s (%s) -> unknown_flag (%s)' % ( print('creating symlink %s (%s) -> unknown_flag (%s)' % (
ali_str, ali, dst_name)) ali_str, ali, dst_name))
os.symlink(dst_path, path.join(dst, ali_name)) os.symlink(dst_path, path.join(dst, ali_name))
def materialize_images(src, dst): def materialize_images(src, dst):
code_strings = _copy_files(src, dst) code_strings = _copy_files(src, dst)
_alias_people(code_strings, dst) _alias_people(code_strings, dst)
_alias_flags(code_strings, dst) _alias_flags(code_strings, dst)
_alias_omitted_flags(code_strings, dst) _alias_omitted_flags(code_strings, dst)
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-s', '--srcdir', help='path to input sources', metavar='dir', '-s', '--srcdir', help='path to input sources', metavar='dir',
default = 'build/compressed_pngs') default = 'build/compressed_pngs')
parser.add_argument( parser.add_argument(
'-d', '--dstdir', help='destination for output images', metavar='dir') '-d', '--dstdir', help='destination for output images', metavar='dir')
args = parser.parse_args() args = parser.parse_args()
materialize_images(args.srcdir, args.dstdir) materialize_images(args.srcdir, args.dstdir)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,88 +1,88 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright 2017 Google Inc. All rights reserved. # Copyright 2017 Google Inc. All rights reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import glob import glob
import os import os
from os import path from os import path
import sys import sys
"""Rename image files based on codepoints to remove the emoji variation """Rename image files based on codepoints to remove the emoji variation
selector from the name. For our emoji image data, this codepoint is not selector from the name. For our emoji image data, this codepoint is not
relevant.""" relevant."""
EMOJI_VS = 0xfe0f EMOJI_VS = 0xfe0f
def str_to_seq(seq_str): def str_to_seq(seq_str):
return tuple([int(s, 16) for s in seq_str.split('_')]) return tuple([int(s, 16) for s in seq_str.split('_')])
def seq_to_str(seq): def seq_to_str(seq):
return '_'.join('%04x' % cp for cp in seq) return '_'.join('%04x' % cp for cp in seq)
def strip_vs(seq): def strip_vs(seq):
return tuple([cp for cp in seq if cp != EMOJI_VS]) return tuple([cp for cp in seq if cp != EMOJI_VS])
def strip_vs_from_filenames(imagedir, prefix, ext, dry_run=False): def strip_vs_from_filenames(imagedir, prefix, ext, dry_run=False):
prefix_len = len(prefix) prefix_len = len(prefix)
suffix_len = len(ext) + 1 suffix_len = len(ext) + 1
names = [path.basename(f) names = [path.basename(f)
for f in glob.glob( for f in glob.glob(
path.join(imagedir, '%s*.%s' % (prefix, ext)))] path.join(imagedir, '%s*.%s' % (prefix, ext)))]
renames = {} renames = {}
for name in names: for name in names:
seq = str_to_seq(name[prefix_len:-suffix_len]) seq = str_to_seq(name[prefix_len:-suffix_len])
if seq and EMOJI_VS in seq: if seq and EMOJI_VS in seq:
newname = '%s%s.%s' % (prefix, seq_to_str(strip_vs(seq)), ext) newname = '%s%s.%s' % (prefix, seq_to_str(strip_vs(seq)), ext)
if newname in names: if newname in names:
print('%s non-vs name %s already exists.' % ( print('%s non-vs name %s already exists.' % (
name, newname), file=sys.stderr) name, newname), file=sys.stderr)
return return
renames[name] = newname renames[name] = newname
for k, v in renames.iteritems(): for k, v in renames.iteritems():
if dry_run: if dry_run:
print('%s -> %s' % (k, v)) print('%s -> %s' % (k, v))
else: else:
os.rename(path.join(imagedir, k), path.join(imagedir, v)) os.rename(path.join(imagedir, k), path.join(imagedir, v))
print('renamed %d files in %s' % (len(renames), imagedir)) print('renamed %d files in %s' % (len(renames), imagedir))
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'-d', '--imagedir', help='directory containing images to rename', '-d', '--imagedir', help='directory containing images to rename',
metavar='dir', required=True) metavar='dir', required=True)
parser.add_argument( parser.add_argument(
'-e', '--ext', help='image filename extension (default png)', '-e', '--ext', help='image filename extension (default png)',
choices=['ai', 'png', 'svg'], default='png') choices=['ai', 'png', 'svg'], default='png')
parser.add_argument( parser.add_argument(
'-p', '--prefix', help='image filename prefix (default emoji_u)', '-p', '--prefix', help='image filename prefix (default emoji_u)',
default='emoji_u', metavar='pfx') default='emoji_u', metavar='pfx')
parser.add_argument( parser.add_argument(
'-n', '--dry_run', help='compute renames and list only', '-n', '--dry_run', help='compute renames and list only',
action='store_true') action='store_true')
args = parser.parse_args() args = parser.parse_args()
strip_vs_from_filenames(args.imagedir, args.prefix, args.ext, args.dry_run) strip_vs_from_filenames(args.imagedir, args.prefix, args.ext, args.dry_run)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -1,198 +1,198 @@
# Copyright 2015 Google, Inc. All Rights Reserved. # Copyright 2015 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Doug Felt # Google Author(s): Doug Felt
import math import math
import random import random
import re import re
import string import string
import svg_cleaner import svg_cleaner
class SvgBuilder(object): class SvgBuilder(object):
"""Modifies a font to add SVG glyphs from a document or string. Once built you """Modifies a font to add SVG glyphs from a document or string. Once built you
can call add_from_filename or add_from_doc multiple times to add SVG can call add_from_filename or add_from_doc multiple times to add SVG
documents, which should contain a single root svg element representing the glyph. documents, which should contain a single root svg element representing the glyph.
This element must have width and height attributes (in px), these are used to This element must have width and height attributes (in px), these are used to
determine how to scale the glyph. The svg should be designed to fit inside determine how to scale the glyph. The svg should be designed to fit inside
this bounds and have its origin at the top left. Adding the svg generates a this bounds and have its origin at the top left. Adding the svg generates a
transform to scale and position the glyph, so the svg element should not have transform to scale and position the glyph, so the svg element should not have
a transform attribute since it will be overwritten. Any id attribute on the a transform attribute since it will be overwritten. Any id attribute on the
glyph is also overwritten. glyph is also overwritten.
Adding a glyph can generate additional default glyphs for components of a Adding a glyph can generate additional default glyphs for components of a
ligature that are not already present. ligature that are not already present.
It is possible to add SVG images to a font that already has corresponding It is possible to add SVG images to a font that already has corresponding
glyphs. If a glyph exists already, then its hmtx advance is assumed valid. glyphs. If a glyph exists already, then its hmtx advance is assumed valid.
Otherwise we will generate an advance based on the image's width and scale Otherwise we will generate an advance based on the image's width and scale
factor. Callers should ensure that glyphs for components of ligatures are factor. Callers should ensure that glyphs for components of ligatures are
added before the ligatures themselves, otherwise glyphs generated for missing added before the ligatures themselves, otherwise glyphs generated for missing
ligature components will be assigned zero metrics metrics that will not be ligature components will be assigned zero metrics metrics that will not be
overridden later.""" overridden later."""
def __init__(self, font_builder): def __init__(self, font_builder):
font_builder.init_svg() font_builder.init_svg()
self.font_builder = font_builder self.font_builder = font_builder
self.cleaner = svg_cleaner.SvgCleaner() self.cleaner = svg_cleaner.SvgCleaner()
font = font_builder.font font = font_builder.font
self.font_ascent = font['hhea'].ascent self.font_ascent = font['hhea'].ascent
self.font_height = self.font_ascent - font['hhea'].descent self.font_height = self.font_ascent - font['hhea'].descent
self.font_upem = font['head'].unitsPerEm self.font_upem = font['head'].unitsPerEm
def add_from_filename(self, ustr, filename): def add_from_filename(self, ustr, filename):
with open(filename, "r") as fp: with open(filename, "r") as fp:
return self.add_from_doc(ustr, fp.read(), filename=filename) return self.add_from_doc(ustr, fp.read(), filename=filename)
def _strip_px(self, val): def _strip_px(self, val):
return float(val[:-2] if val.endswith('px') else val) return float(val[:-2] if val.endswith('px') else val)
def add_from_doc(self, ustr, svgdoc, filename=None): def add_from_doc(self, ustr, svgdoc, filename=None):
"""Cleans the svg doc, tweaks the root svg element's """Cleans the svg doc, tweaks the root svg element's
attributes, then updates the font. ustr is the character or ligature attributes, then updates the font. ustr is the character or ligature
string, svgdoc is the svg document xml. The doc must have a single string, svgdoc is the svg document xml. The doc must have a single
svg root element.""" svg root element."""
# The svg element must have an id attribute of the form 'glyphNNN' where NNN # The svg element must have an id attribute of the form 'glyphNNN' where NNN
# is the glyph id. We capture the index of the glyph we're adding and write # is the glyph id. We capture the index of the glyph we're adding and write
# it into the svg. # it into the svg.
# #
# We generate a transform that places the origin at the top left of the # We generate a transform that places the origin at the top left of the
# ascent and uniformly scales it to fit both the font height (ascent - # ascent and uniformly scales it to fit both the font height (ascent -
# descent) and glyph advance if it is already present. The initial viewport # descent) and glyph advance if it is already present. The initial viewport
# is 1000x1000. When present, viewBox scales to fit this and uses default # is 1000x1000. When present, viewBox scales to fit this and uses default
# values for preserveAspectRatio that center the viewBox in this viewport # values for preserveAspectRatio that center the viewBox in this viewport
# ('xMidyMid meet'), and ignores the width and height. If viewBox is not # ('xMidyMid meet'), and ignores the width and height. If viewBox is not
# present, width and height cause a (possibly non-uniform) scale to be # present, width and height cause a (possibly non-uniform) scale to be
# applied that map the extent to the viewport. This is unfortunate for us, # applied that map the extent to the viewport. This is unfortunate for us,
# since we want to preserve the aspect ratio, and the image is likely # since we want to preserve the aspect ratio, and the image is likely
# designed for a viewport with the width and height it requested. # designed for a viewport with the width and height it requested.
# #
# If we have an advance, we want to replicate the behavior of viewBox, # If we have an advance, we want to replicate the behavior of viewBox,
# except using a 'viewport' of advance, ascent+descent. If we don't have # except using a 'viewport' of advance, ascent+descent. If we don't have
# an advance, we scale the height and compute the advance from the scaled # an advance, we scale the height and compute the advance from the scaled
# width. # width.
# #
# Lengths using percentage units map 100% to the width/height/diagonal # Lengths using percentage units map 100% to the width/height/diagonal
# of the viewBox, or if it is not defined, the viewport. Since we can't # of the viewBox, or if it is not defined, the viewport. Since we can't
# define the viewport, we must always have a viewBox. # define the viewport, we must always have a viewBox.
cleaner = self.cleaner cleaner = self.cleaner
fbuilder = self.font_builder fbuilder = self.font_builder
tree = cleaner.tree_from_text(svgdoc) tree = cleaner.tree_from_text(svgdoc)
name, index, exists = fbuilder.add_components_and_ligature(ustr) name, index, exists = fbuilder.add_components_and_ligature(ustr)
advance = 0 advance = 0
if exists: if exists:
advance = fbuilder.hmtx[name][0] advance = fbuilder.hmtx[name][0]
vb = tree.attrs.get('viewBox') vb = tree.attrs.get('viewBox')
if vb: if vb:
x, y, w, h = map(self._strip_px, re.split('\s*,\s*|\s+', vb)) x, y, w, h = map(self._strip_px, re.split('\s*,\s*|\s+', vb))
else: else:
wid = tree.attrs.get('width') wid = tree.attrs.get('width')
ht = tree.attrs.get('height') ht = tree.attrs.get('height')
if not (wid and ht): if not (wid and ht):
raise ValueError( raise ValueError(
'missing viewBox and width or height attrs (%s)' % filename) 'missing viewBox and width or height attrs (%s)' % filename)
x, y, w, h = 0, 0, self._strip_px(wid), self._strip_px(ht) x, y, w, h = 0, 0, self._strip_px(wid), self._strip_px(ht)
# We're going to assume default values for preserveAspectRatio for now, # We're going to assume default values for preserveAspectRatio for now,
# this preserves aspect ratio and centers in the viewport. # this preserves aspect ratio and centers in the viewport.
# #
# The viewport is 0,0 1000x1000. First compute the scaled extent and # The viewport is 0,0 1000x1000. First compute the scaled extent and
# translations that center the image rect in the viewport, then scale and # translations that center the image rect in the viewport, then scale and
# translate the result to fit our true 'viewport', which has an origin at # translate the result to fit our true 'viewport', which has an origin at
# 0,-ascent and an extent of advance (if defined) x font_height. We won't # 0,-ascent and an extent of advance (if defined) x font_height. We won't
# try to optimize this, it's clearer what we're doing this way. # try to optimize this, it's clearer what we're doing this way.
# Since the viewport is square, we can just compare w and h to determine # Since the viewport is square, we can just compare w and h to determine
# which to fit to the viewport extent. Get our position and extent in # which to fit to the viewport extent. Get our position and extent in
# the viewport. # the viewport.
if w > h: if w > h:
scale_to_viewport = 1000.0 / w scale_to_viewport = 1000.0 / w
h_in_viewport = scale_to_viewport * h h_in_viewport = scale_to_viewport * h
y_in_viewport = (1000 - h_in_viewport) / 2 y_in_viewport = (1000 - h_in_viewport) / 2
w_in_viewport = 1000.0 w_in_viewport = 1000.0
x_in_viewport = 0.0 x_in_viewport = 0.0
else: else:
scale_to_viewport = 1000.0 / h scale_to_viewport = 1000.0 / h
h_in_viewport = 1000.0 h_in_viewport = 1000.0
y_in_viewport = 0.0 y_in_viewport = 0.0
w_in_viewport = scale_to_viewport * w w_in_viewport = scale_to_viewport * w
x_in_viewport = (1000 - w_in_viewport) / 2 x_in_viewport = (1000 - w_in_viewport) / 2
# Now, compute the scale and translations that fit this rectangle to our # Now, compute the scale and translations that fit this rectangle to our
# true 'viewport'. The true viewport is not square so we need to choose the # true 'viewport'. The true viewport is not square so we need to choose the
# smaller of the scales that fit its height or width. We start with height, # smaller of the scales that fit its height or width. We start with height,
# if there's no advance then we're done, otherwise we might have to fit the # if there's no advance then we're done, otherwise we might have to fit the
# advance. # advance.
scale = self.font_height / h_in_viewport scale = self.font_height / h_in_viewport
fit_height = True fit_height = True
if advance and scale * w_in_viewport > advance: if advance and scale * w_in_viewport > advance:
scale = advance / w_in_viewport scale = advance / w_in_viewport
fit_height = False fit_height = False
# Compute transforms that put the top left of the image where we want it. # Compute transforms that put the top left of the image where we want it.
ty = -self.font_ascent - scale * y_in_viewport ty = -self.font_ascent - scale * y_in_viewport
tx = -scale * x_in_viewport tx = -scale * x_in_viewport
# Adjust them to center the image horizontally if we fit the full height, # Adjust them to center the image horizontally if we fit the full height,
# vertically otherwise. # vertically otherwise.
if fit_height and advance: if fit_height and advance:
tx += (advance - scale * w_in_viewport) / 2 tx += (advance - scale * w_in_viewport) / 2
else: else:
ty += (self.font_height - scale * h_in_viewport) / 2 ty += (self.font_height - scale * h_in_viewport) / 2
cleaner.clean_tree(tree) cleaner.clean_tree(tree)
tree.attrs['id'] = 'glyph%s' % index tree.attrs['id'] = 'glyph%s' % index
transform = 'translate(%g, %g) scale(%g)' % (tx, ty, scale) transform = 'translate(%g, %g) scale(%g)' % (tx, ty, scale)
tree.attrs['transform'] = transform tree.attrs['transform'] = transform
tree.attrs['viewBox'] = '%g %g %g %g' % (x, y, w, h) tree.attrs['viewBox'] = '%g %g %g %g' % (x, y, w, h)
# In order to clip, we need to create a path and reference it. You'd think # In order to clip, we need to create a path and reference it. You'd think
# establishing a rectangular clip would be simpler... Aaaaand... as it # establishing a rectangular clip would be simpler... Aaaaand... as it
# turns out, in FF the clip on the outer svg element is only relative to the # turns out, in FF the clip on the outer svg element is only relative to the
# initial viewport, and is not affected by the viewBox or transform on the # initial viewport, and is not affected by the viewBox or transform on the
# svg element. Unlike chrome. So either we apply an inverse transform, or # svg element. Unlike chrome. So either we apply an inverse transform, or
# insert a group with the clip between the svg and its children. The latter # insert a group with the clip between the svg and its children. The latter
# seems cleaner, ultimately. # seems cleaner, ultimately.
clip_id = 'clip_' + ''.join( clip_id = 'clip_' + ''.join(
random.choice(string.ascii_lowercase) for i in range(8)) random.choice(string.ascii_lowercase) for i in range(8))
clip_text = ('<g clip-path="url(#%s)"><clipPath id="%s">' clip_text = ('<g clip-path="url(#%s)"><clipPath id="%s">'
'<path d="M%g %gh%gv%gh%gz"/></clipPath></g>' % ( '<path d="M%g %gh%gv%gh%gz"/></clipPath></g>' % (
clip_id, clip_id, x, y, w, h, -w)) clip_id, clip_id, x, y, w, h, -w))
clip_tree = cleaner.tree_from_text(clip_text) clip_tree = cleaner.tree_from_text(clip_text)
clip_tree.contents.extend(tree.contents) clip_tree.contents.extend(tree.contents)
tree.contents = [clip_tree] tree.contents = [clip_tree]
svgdoc = cleaner.tree_to_text(tree) svgdoc = cleaner.tree_to_text(tree)
hmetrics = None hmetrics = None
if not exists: if not exists:
# There was no advance to fit, so no horizontal centering. The image advance is # There was no advance to fit, so no horizontal centering. The image advance is
# all there is. # all there is.
# hmetrics is horiz advance and lsb # hmetrics is horiz advance and lsb
advance = scale * w_in_viewport advance = scale * w_in_viewport
hmetrics = [int(round(advance)), 0] hmetrics = [int(round(advance)), 0]
fbuilder.add_svg(svgdoc, hmetrics, name, index) fbuilder.add_svg(svgdoc, hmetrics, name, index)

View file

@ -1,338 +1,357 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Copyright 2015 Google, Inc. All Rights Reserved. # Copyright 2015 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Doug Felt # Google Author(s): Doug Felt
"""Clean SVG. """Clean SVG.
svgo could do this, but we're fussy. Also, emacs doesn't understand svgo could do this, but we're fussy. Also, emacs doesn't understand
that 'style' defaults to 'text/css' and svgo strips this out by that 'style' defaults to 'text/css' and svgo strips this out by
default. default.
The files we're getting that are exported from AI contain lots of extra The files we're getting that are exported from AI contain lots of extra
data so that it can reimport the svg, and we don't need it.""" data so that it can reimport the svg, and we don't need it."""
import argparse import argparse
import codecs import codecs
import logging import logging
import os import os
from os import path from os import path
import re import re
import sys import sys
from nototools import tool_utils from nototools import tool_utils
from xml.parsers import expat from xml.parsers import expat
from xml.sax import saxutils from xml.sax import saxutils
# Expat doesn't allow me to identify empty tags (in particular, with an # Expat doesn't allow me to identify empty tags (in particular, with an
# empty tag the parse location for the start and end is not the same) so I # empty tag the parse location for the start and end is not the same) so I
# have to take a dom-like approach if I want to identify them. There are a # have to take a dom-like approach if I want to identify them. There are a
# lot of empty tags in svg. This way I can do some other kinds of cleanup # lot of empty tags in svg. This way I can do some other kinds of cleanup
# as well (remove unnecessary 'g' elements, for instance). # as well (remove unnecessary 'g' elements, for instance).
# Use nodes instead of tuples and strings because it's easier to mutate # Use nodes instead of tuples and strings because it's easier to mutate
# a tree of these, and cleaner will want to do this. # a tree of these, and cleaner will want to do this.
class _Elem_Node(object): class _Elem_Node(object):
def __init__(self, name, attrs, contents): def __init__(self, name, attrs, contents):
self.name = name self.name = name
self.attrs = attrs self.attrs = attrs
self.contents = contents self.contents = contents
def __repr__(self): def __repr__(self):
line = ["elem(name: '%s'" % self.name] line = ["elem(name: '%s'" % self.name]
if self.attrs: if self.attrs:
line.append(" attrs: '%s'" % self.attrs) line.append(" attrs: '%s'" % self.attrs)
if self.contents: if self.contents:
line.append(" contents[%s]: '%s'" % (len(self.contents), self.contents)) line.append(" contents[%s]: '%s'" % (len(self.contents), self.contents))
line.append(')') line.append(')')
return ''.join(line) return ''.join(line)
class _Text_Node(object): class _Text_Node(object):
def __init__(self, text): def __init__(self, text):
self.text = text self.text = text
def __repr__(self): def __repr__(self):
return "text('%s')" % self.text return "text('%s')" % self.text
class SvgCleaner(object): class SvgCleaner(object):
"""Strip out unwanted parts of an svg file, primarily the xml declaration and """Strip out unwanted parts of an svg file, primarily the xml declaration and
doctype lines, comments, and some attributes of the outermost <svg> element. doctype lines, comments, and some attributes of the outermost <svg> element.
The id will be replaced when it is inserted into the font. viewBox causes The id will be replaced when it is inserted into the font. (viewBox causes
unwanted scaling when used in a font and its effect is difficult to unwanted scaling when used in a font and its effect is difficult to
predict. version is unneeded, xml:space is ignored (we're processing spaces predict, but for outside a font we need to keep it sometimes so we keep it).
so a request to maintain them has no effect). enable-background appears to version is unneeded, xml:space is ignored (we're processing spaces
have no effect. x and y on the outermost svg element have no effect. We so a request to maintain them has no effect). enable-background appears to
keep width and height, and will elsewhere assume these are the dimensions have no effect. x and y on the outermost svg element have no effect. We
used for the character box.""" keep width and height, and will elsewhere assume these are the dimensions
used for the character box."""
def __init__(self):
self.reader = SvgCleaner._Reader() def __init__(self, strip=False):
self.cleaner = SvgCleaner._Cleaner() self.reader = SvgCleaner._Reader()
self.writer = SvgCleaner._Writer() self.cleaner = SvgCleaner._Cleaner()
self.writer = SvgCleaner._Writer(strip)
class _Reader(object):
"""Loosely based on fonttools's XMLReader. This generates a tree of nodes, class _Reader(object):
either element nodes or text nodes. Successive text content is merged """Loosely based on fonttools's XMLReader. This generates a tree of nodes,
into one node, so contents will never contain more than one _Text_Node in either element nodes or text nodes. Successive text content is merged
a row. This drops comments, xml declarations, and doctypes.""" into one node, so contents will never contain more than one _Text_Node in
a row. This drops comments, xml declarations, and doctypes."""
def _reset(self, parser):
self._stack = [] def _reset(self, parser):
self._textbuf = [] self._stack = []
self._textbuf = []
def _start_element(self, name, attrs):
self._flush_textbuf() def _start_element(self, name, attrs):
node = _Elem_Node(name, attrs, []) self._flush_textbuf()
if len(self._stack): node = _Elem_Node(name, attrs, [])
self._stack[-1].contents.append(node) if len(self._stack):
self._stack.append(node) self._stack[-1].contents.append(node)
self._stack.append(node)
def _end_element(self, name):
self._flush_textbuf() def _end_element(self, name):
if len(self._stack) > 1: self._flush_textbuf()
self._stack = self._stack[:-1] if len(self._stack) > 1:
self._stack = self._stack[:-1]
def _character_data(self, data):
if len(self._stack): def _character_data(self, data):
self._textbuf.append(data) if len(self._stack):
self._textbuf.append(data)
def _flush_textbuf(self):
if self._textbuf: def _flush_textbuf(self):
node = _Text_Node(''.join(self._textbuf)) if self._textbuf:
self._stack[-1].contents.append(node) node = _Text_Node(''.join(self._textbuf))
self._textbuf = [] self._stack[-1].contents.append(node)
self._textbuf = []
def from_text(self, data):
"""Return the root node of a tree representing the svg data.""" def from_text(self, data):
"""Return the root node of a tree representing the svg data."""
parser = expat.ParserCreate()
parser.StartElementHandler = self._start_element parser = expat.ParserCreate()
parser.EndElementHandler = self._end_element parser.StartElementHandler = self._start_element
parser.CharacterDataHandler = self._character_data parser.EndElementHandler = self._end_element
self._reset(parser) parser.CharacterDataHandler = self._character_data
parser.Parse(data) self._reset(parser)
return self._stack[0] parser.Parse(data)
return self._stack[0]
class _Cleaner(object):
def _clean_elem(self, node): class _Cleaner(object):
viewBox, width, height = None, None, None def _clean_elem(self, node):
nattrs = {} viewBox, x, y, width, height = None, None, None, None, None
for k, v in node.attrs.items(): nattrs = {}
if node.name == 'svg' and k in [ for k, v in node.attrs.items():
'x', 'y', 'id', 'version', 'viewBox', 'width', 'height', if node.name == 'svg' and k in [
'enable-background', 'xml:space', 'xmlns:graph', 'xmlns:i', 'x', 'y', 'id', 'version', 'viewBox', 'width', 'height',
'xmlns:x']: 'enable-background', 'xml:space', 'xmlns:graph', 'xmlns:i',
if k == 'viewBox': 'xmlns:x']:
viewBox = v if k == 'viewBox':
elif k == 'width': viewBox = v
width = v elif k == 'width':
elif k == 'height': width = v
height = v elif k == 'height':
elif k.startswith('xmlns:') and 'ns.adobe.com' not in v: height = v
# keep if not an adobe namespace elif k.startswith('xmlns:') and 'ns.adobe.com' not in v:
logging.debug('keep "%s" = "%s"' % (k, v)) # keep if not an adobe namespace
nattrs[k] = v logging.debug('keep "%s" = "%s"' % (k, v))
logging.debug('removing %s=%s' % (k, v)) nattrs[k] = v
continue logging.debug('removing %s=%s' % (k, v))
v = re.sub('\s+', ' ', v) continue
nattrs[k] = v v = re.sub('\s+', ' ', v)
nattrs[k] = v
if node.name == 'svg':
if not width or not height: if node.name == 'svg':
if not viewBox: if viewBox:
raise ValueError('no viewBox, width, or height') x, y, width, height = viewBox.split()
width, height = viewBox.split()[2:] if not width or not height:
nattrs['width'] = width if not viewBox:
nattrs['height'] = height raise ValueError('no viewBox, width, or height')
node.attrs = nattrs nattrs['width'] = width
nattrs['height'] = height
# scan contents. remove any empty text nodes, or empty 'g' element nodes. # keep for svg use outside of font
# if a 'g' element has no attrs and only one subnode, replace it with the if viewBox and (int(x) != 0 or int(y) != 0):
# subnode. logging.warn('viewbox "%s" x: %s y: %s' % (viewBox, x, y));
wpos = 0 nattrs['viewBox'] = viewBox
for n in node.contents: node.attrs = nattrs
if isinstance(n, _Text_Node):
if not n.text: # if display:none, skip this and its children
continue style = node.attrs.get('style')
elif n.name == 'g': if (style and 'display:none' in style) or node.attrs.get('display') == 'none':
if not n.contents: node.contents = []
continue return
if 'i:extraneous' in n.attrs:
del n.attrs['i:extraneous'] # scan contents. remove any empty text nodes, or empty 'g' element nodes.
if not n.attrs and len(n.contents) == 1: # if a 'g' element has no attrs and only one subnode, replace it with the
n = n.contents[0] # subnode.
elif n.name == 'i:pgf' or n.name == 'foreignObject': wpos = 0
continue for n in node.contents:
elif n.name =='switch' and len(n.contents) == 1: if isinstance(n, _Text_Node):
n = n.contents[0] if not n.text:
elif n.name == 'style': continue
# some emacsen don't default 'style' properly, so leave this in. elif n.name == 'g':
if False and n.attrs.get('type') == 'text/css': if not n.contents:
del n.attrs['type'] continue
if 'i:extraneous' in n.attrs:
node.contents[wpos] = n del n.attrs['i:extraneous']
wpos += 1 if not n.attrs and len(n.contents) == 1:
if wpos < len(node.contents): n = n.contents[0]
node.contents = node.contents[:wpos] elif n.name == 'i:pgf' or n.name == 'foreignObject':
continue
def _clean_text(self, node): elif n.name =='switch' and len(n.contents) == 1:
text = node.text.strip() n = n.contents[0]
# common case is text is empty (line endings between elements) elif n.name == 'style':
if text: # some emacsen don't default 'style' properly, so leave this in.
# main goal here is to leave linefeeds in for style elements if False and n.attrs.get('type') == 'text/css':
text = re.sub(r'[ \t]*\n+[ \t]*', '\n', text) del n.attrs['type']
text = re.sub(r'[ \t]+', ' ', text)
node.text = text node.contents[wpos] = n
wpos += 1
def clean(self, node): if wpos < len(node.contents):
if isinstance(node, _Text_Node): node.contents = node.contents[:wpos]
self._clean_text(node)
else: def _clean_text(self, node):
# do contents first, so we can check for empty subnodes after text = node.text.strip()
for n in node.contents: # common case is text is empty (line endings between elements)
self.clean(n) if text:
self._clean_elem(node) # main goal here is to leave linefeeds in for style elements
text = re.sub(r'[ \t]*\n+[ \t]*', '\n', text)
class _Writer(object): text = re.sub(r'[ \t]+', ' ', text)
"""For text nodes, replaces sequences of whitespace with a single space. node.text = text
For elements, replaces sequences of whitespace in attributes, and
removes unwanted attributes from <svg> elements.""" def clean(self, node):
if isinstance(node, _Text_Node):
def _write_node(self, node, lines, indent): self._clean_text(node)
"""Node is a node generated by _Reader, either a TextNode or an else:
ElementNode. Lines is a list to collect the lines of output. Indent is # do contents first, so we can check for empty subnodes after
the indentation level for this node.""" for n in node.contents:
self.clean(n)
if isinstance(node, _Text_Node): self._clean_elem(node)
if node.text:
lines.append(node.text) class _Writer(object):
else: """For text nodes, replaces sequences of whitespace with a single space.
margin = ' ' * indent For elements, replaces sequences of whitespace in attributes, and
line = [margin] removes unwanted attributes from <svg> elements."""
line.append('<%s' % node.name) def __init__(self, strip):
# custom sort attributes of svg, yes this is a hack logging.warning('writer strip: %s' % strip);
if node.name == 'svg': self._strip = strip
def svgsort(k):
if k == 'width': return (0, None) def _write_node(self, node, lines, indent):
elif k == 'height': return (1, None) """Node is a node generated by _Reader, either a TextNode or an
else: return (2, k) ElementNode. Lines is a list to collect the lines of output. Indent is
ks = sorted(node.attrs.keys(), key=svgsort) the indentation level for this node."""
else:
def defsort(k): if isinstance(node, _Text_Node):
if k == 'id': return (0, None) if node.text:
elif k == 'class': return (1, None) lines.append(node.text)
else: return (2, k) else:
ks = sorted(node.attrs.keys(), key=defsort) margin = '' if self._strip else ' ' * indent
for k in ks: line = [margin]
v = node.attrs[k] line.append('<%s' % node.name)
line.append(' %s=%s' % (k, saxutils.quoteattr(v))) # custom sort attributes of svg, yes this is a hack
if node.contents: if node.name == 'svg':
line.append('>') def svgsort(k):
lines.append(''.join(line)) if k == 'width': return (0, None)
for elem in node.contents: elif k == 'height': return (1, None)
self._write_node(elem, lines, indent + 1) else: return (2, k)
line = [margin] ks = sorted(node.attrs.keys(), key=svgsort)
line.append('</%s>' % node.name) else:
lines.append(''.join(line)) def defsort(k):
else: if k == 'id': return (0, None)
line.append('/>') elif k == 'class': return (1, None)
lines.append(''.join(line)) else: return (2, k)
ks = sorted(node.attrs.keys(), key=defsort)
def to_text(self, root): for k in ks:
# set up lines for recursive calls, let them append lines, then return v = node.attrs[k]
# the result. line.append(' %s=%s' % (k, saxutils.quoteattr(v)))
lines = [] if node.contents:
self._write_node(root, lines, 0) line.append('>')
return '\n'.join(lines) lines.append(''.join(line))
for elem in node.contents:
def tree_from_text(self, svg_text): self._write_node(elem, lines, indent + 1)
return self.reader.from_text(svg_text) line = [margin]
line.append('</%s>' % node.name)
def clean_tree(self, svg_tree): lines.append(''.join(line))
self.cleaner.clean(svg_tree) else:
line.append('/>')
def tree_to_text(self, svg_tree): lines.append(''.join(line))
return self.writer.to_text(svg_tree)
def to_text(self, root):
def clean_svg(self, svg_text): # set up lines for recursive calls, let them append lines, then return
"""Return the cleaned svg_text.""" # the result.
tree = self.tree_from_text(svg_text) lines = []
self.clean_tree(tree) self._write_node(root, lines, 0)
return self.tree_to_text(tree) return ''.join(lines) if self._strip else '\n'.join(lines)
def tree_from_text(self, svg_text):
def clean_svg_files(in_dir, out_dir, match_pat=None, clean=False): return self.reader.from_text(svg_text)
regex = re.compile(match_pat) if match_pat else None
count = 0 def clean_tree(self, svg_tree):
self.cleaner.clean(svg_tree)
if clean and path.samefile(in_dir, out_dir):
logging.error('Cannot clean %s (same as in_dir)', out_dir) def tree_to_text(self, svg_tree):
return return self.writer.to_text(svg_tree)
out_dir = tool_utils.ensure_dir_exists(out_dir, clean=clean) def clean_svg(self, svg_text):
"""Return the cleaned svg_text."""
cleaner = SvgCleaner() tree = self.tree_from_text(svg_text)
for file_name in os.listdir(in_dir): self.clean_tree(tree)
if regex and not regex.match(file_name): return self.tree_to_text(tree)
continue
in_path = os.path.join(in_dir, file_name)
logging.debug('read: %s', in_path) def clean_svg_files(in_dir, out_dir, match_pat=None, clean=False, strip=False):
with open(in_path) as in_fp: regex = re.compile(match_pat) if match_pat else None
result = cleaner.clean_svg(in_fp.read()) count = 0
out_path = os.path.join(out_dir, file_name)
with codecs.open(out_path, 'w', 'utf-8') as out_fp: if clean and path.samefile(in_dir, out_dir):
logging.debug('write: %s', out_path) logging.error('Cannot clean %s (same as in_dir)', out_dir)
out_fp.write(result) return
count += 1
if not count: out_dir = tool_utils.ensure_dir_exists(out_dir, clean=clean)
logging.warning('Failed to match any files')
else: cleaner = SvgCleaner(strip)
logging.info('Wrote %s files to %s', count, out_dir) for file_name in os.listdir(in_dir):
if regex and not regex.match(file_name):
continue
def main(): in_path = os.path.join(in_dir, file_name)
parser = argparse.ArgumentParser( logging.debug('read: %s', in_path)
description="Generate 'cleaned' svg files.") with open(in_path) as in_fp:
parser.add_argument( result = cleaner.clean_svg(in_fp.read())
'in_dir', help='Input directory.', metavar='dir') out_path = os.path.join(out_dir, file_name)
parser.add_argument( with codecs.open(out_path, 'w', 'utf-8') as out_fp:
'-o', '--out_dir', help='Output directory, defaults to sibling of in_dir', logging.debug('write: %s', out_path)
metavar='dir') out_fp.write(result)
parser.add_argument( count += 1
'-c', '--clean', help='Clean output directory', action='store_true') if not count:
parser.add_argument( logging.warning('Failed to match any files')
'-r', '--regex', help='Regex to select files, default matches all files.', else:
metavar='regex', default=None) logging.info('Wrote %s files to %s', count, out_dir)
parser.add_argument(
'-l', '--loglevel', help='log level name/value', default='warning')
args = parser.parse_args() def main():
parser = argparse.ArgumentParser(
tool_utils.setup_logging(args.loglevel) description="Generate 'cleaned' svg files.")
parser.add_argument(
if not args.out_dir: 'in_dir', help='Input directory.', metavar='dir')
if args.in_dir.endswith('/'): parser.add_argument(
args.in_dir = args.in_dir[:-1] '-o', '--out_dir', help='Output directory, defaults to sibling of in_dir',
args.out_dir = args.in_dir + '_clean' metavar='dir')
logging.info('Writing output to %s', args.out_dir) parser.add_argument(
'-c', '--clean', help='Clean output directory', action='store_true')
clean_svg_files( parser.add_argument(
args.in_dir, args.out_dir, match_pat=args.regex, clean=args.clean) '-r', '--regex', help='Regex to select files, default matches all files.',
metavar='regex', default=None)
parser.add_argument(
if __name__ == '__main__': '-l', '--loglevel', help='log level name/value', default='warning')
main() parser.add_argument(
'-w', '--strip_whitespace', help='remove newlines and indentation',
action='store_true')
args = parser.parse_args()
tool_utils.setup_logging(args.loglevel)
if not args.out_dir:
if args.in_dir.endswith('/'):
args.in_dir = args.in_dir[:-1]
args.out_dir = args.in_dir + '_clean'
logging.info('Writing output to %s', args.out_dir)
clean_svg_files(
args.in_dir, args.out_dir, match_pat=args.regex, clean=args.clean,
strip=args.strip_whitespace)
if __name__ == '__main__':
main()

View file

@ -1,13 +1,13 @@
Copyright 2013 Google, Inc. All Rights Reserved. Copyright 2013 Google, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.

View file

@ -1,13 +1,13 @@
This project consists of the following bits and pieces: This project consists of the following bits and pieces:
* A proposed specification to add support for embedded color image * A proposed specification to add support for embedded color image
glyphs in OpenType fonts, glyphs in OpenType fonts,
* A tool called emoji_builder.py, to embed a set of PNG images into * A tool called emoji_builder.py, to embed a set of PNG images into
an existing font, an existing font,
* Two sets of sample PNG images for ASCII characters and sample * Two sets of sample PNG images for ASCII characters and sample
scripts to build them into test fonts: FruityGirl and Funkster. scripts to build them into test fonts: FruityGirl and Funkster.
* Scripts to build a real color emoji font out of the Open Source * Scripts to build a real color emoji font out of the Open Source
PhantomOpenEmoji images. PhantomOpenEmoji images.

View file

@ -1,11 +1,11 @@
URL: http://color-emoji.googlecode.com/archive/dce2d8ad953a7b03200723f9f1d25121cb45150a.zip URL: http://color-emoji.googlecode.com/archive/dce2d8ad953a7b03200723f9f1d25121cb45150a.zip
Version: dce2d8ad953a7b03200723f9f1d25121cb45150a Version: dce2d8ad953a7b03200723f9f1d25121cb45150a
License: BSD License: BSD
License File: LICENSE License File: LICENSE
Description: Description:
Color Emoji font creation tools Color Emoji font creation tools
Local Modifications: Local Modifications:
COPYING file was renamed to LICENSE. The samples font sources and the COPYING file was renamed to LICENSE. The samples font sources and the
specification are not included. specification are not included.

File diff suppressed because it is too large Load diff

View file

@ -1,107 +1,116 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright 2013 Google, Inc. All Rights Reserved. # Copyright 2013 Google, Inc. All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# Google Author(s): Behdad Esfahbod # Google Author(s): Behdad Esfahbod
# #
import struct, StringIO import struct
import sys
from io import BytesIO
class PNG:
signature = bytearray ((137,80,78,71,13,10,26,10)) try:
basestring # py2
def __init__ (self, f): except NameError:
basestring = str # py3
if isinstance(f, basestring):
f = open (f, 'rb')
class PNG:
self.f = f
self.IHDR = None signature = bytearray ((137,80,78,71,13,10,26,10))
def tell (self): def __init__ (self, f):
return self.f.tell ()
if isinstance(f, basestring):
def seek (self, pos): f = open (f, 'rb')
self.f.seek (pos)
self.f = f
def stream (self): self.IHDR = None
return self.f
def tell (self):
def data (self): return self.f.tell ()
self.seek (0)
return bytearray (self.f.read ()) def seek (self, pos):
self.f.seek (pos)
class BadSignature (Exception): pass
class BadChunk (Exception): pass def stream (self):
return self.f
def read_signature (self):
header = bytearray (self.f.read (8)) def data (self):
if header != PNG.signature: self.seek (0)
raise PNG.BadSignature return bytearray (self.f.read ())
return PNG.signature
class BadSignature (Exception): pass
def read_chunk (self): class BadChunk (Exception): pass
length = struct.unpack (">I", self.f.read (4))[0]
chunk_type = self.f.read (4) def read_signature (self):
chunk_data = self.f.read (length) header = bytearray (self.f.read (8))
if len (chunk_data) != length: if header != PNG.signature:
raise PNG.BadChunk raise PNG.BadSignature
crc = self.f.read (4) return PNG.signature
if len (crc) != 4:
raise PNG.BadChunk def read_chunk (self):
return (chunk_type, chunk_data, crc) buf = self.f.read (4)
length = struct.unpack (">I", buf)[0]
def read_IHDR (self): chunk_type = self.f.read (4)
(chunk_type, chunk_data, crc) = self.read_chunk () chunk_data = self.f.read (length)
if chunk_type != "IHDR": if len (chunk_data) != length:
raise PNG.BadChunk raise PNG.BadChunk
# Width: 4 bytes crc = self.f.read (4)
# Height: 4 bytes if len (crc) != 4:
# Bit depth: 1 byte raise PNG.BadChunk
# Color type: 1 byte return (chunk_type, chunk_data, crc)
# Compression method: 1 byte
# Filter method: 1 byte def read_IHDR (self):
# Interlace method: 1 byte (chunk_type, chunk_data, crc) = self.read_chunk ()
return struct.unpack (">IIBBBBB", chunk_data) if chunk_type != b"IHDR":
raise PNG.BadChunk
def read_header (self): # Width: 4 bytes
self.read_signature () # Height: 4 bytes
self.IHDR = self.read_IHDR () # Bit depth: 1 byte
return self.IHDR # Color type: 1 byte
# Compression method: 1 byte
def get_size (self): # Filter method: 1 byte
if not self.IHDR: # Interlace method: 1 byte
pos = self.tell () return struct.unpack (">IIBBBBB", chunk_data)
self.seek (0)
self.read_header () def read_header (self):
self.seek (pos) self.read_signature ()
return self.IHDR[0:2] self.IHDR = self.read_IHDR ()
return self.IHDR
def filter_chunks (self, chunks):
self.seek (0); def get_size (self):
out = StringIO.StringIO () if not self.IHDR:
out.write (self.read_signature ()) pos = self.tell ()
while True: self.seek (0)
chunk_type, chunk_data, crc = self.read_chunk () self.read_header ()
if chunk_type in chunks: self.seek (pos)
out.write (struct.pack (">I", len (chunk_data))) return self.IHDR[0:2]
out.write (chunk_type)
out.write (chunk_data) def filter_chunks (self, chunks):
out.write (crc) self.seek (0);
if chunk_type == "IEND": out = BytesIO ()
break out.write (self.read_signature ())
return PNG (out) while True:
chunk_type, chunk_data, crc = self.read_chunk ()
if chunk_type in chunks:
out.write (struct.pack (">I", len (chunk_data)))
out.write (chunk_type)
out.write (chunk_data)
out.write (crc)
if chunk_type == b"IEND":
break
return PNG (out)

View file

@ -1,451 +1,451 @@
/* /*
* Copyright 2014 Google Inc. All rights reserved. * Copyright 2014 Google Inc. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
* *
* Google contributors: Behdad Esfahbod * Google contributors: Behdad Esfahbod
*/ */
#include <cairo.h> #include <cairo.h>
#include <libgen.h> // basename #include <libgen.h> // basename
#include <math.h> #include <math.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#define SCALE 8 #define SCALE 8
#define SIZE 128 #define SIZE 128
#define MARGIN (debug ? 4 : 0) #define MARGIN (debug ? 4 : 0)
static unsigned int debug; static unsigned int debug;
#define std_aspect (5./3.) #define std_aspect (5./3.)
#define top 21 #define top 21
#define bot 128-top #define bot 128-top
#define B 21 #define B 21
#define C 4 #define C 4
static struct { double x, y; } mesh_points[] = static struct { double x, y; } mesh_points[] =
{ {
{ 1, top+C}, { 1, top+C},
{ 43, top-B+C}, { 43, top-B+C},
{ 85, top+B-C}, { 85, top+B-C},
{127, top-C}, {127, top-C},
{127, bot-C}, {127, bot-C},
{ 85, bot+B-C}, { 85, bot+B-C},
{ 43, bot-B+C}, { 43, bot-B+C},
{ 1, bot+C}, { 1, bot+C},
}; };
#define M(i) \ #define M(i) \
x_aspect (mesh_points[i].x, aspect), \ x_aspect (mesh_points[i].x, aspect), \
y_aspect (mesh_points[i].y, aspect) y_aspect (mesh_points[i].y, aspect)
static inline double x_aspect (double v, double aspect) static inline double x_aspect (double v, double aspect)
{ {
return aspect >= 1. ? v : (v - 64) * aspect + 64; return aspect >= 1. ? v : (v - 64) * aspect + 64;
} }
static inline double y_aspect (double v, double aspect) static inline double y_aspect (double v, double aspect)
{ {
return aspect <= 1. ? v : (v - 64) / aspect + 64; return aspect <= 1. ? v : (v - 64) / aspect + 64;
} }
static cairo_path_t * static cairo_path_t *
wave_path_create (double aspect) wave_path_create (double aspect)
{ {
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 0,0); cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 0,0);
cairo_t *cr = cairo_create (surface); cairo_t *cr = cairo_create (surface);
cairo_path_t *path; cairo_path_t *path;
cairo_scale (cr, SIZE/128.*SCALE, SIZE/128.*SCALE); cairo_scale (cr, SIZE/128.*SCALE, SIZE/128.*SCALE);
cairo_line_to(cr, M(0)); cairo_line_to(cr, M(0));
cairo_curve_to(cr, M(1), M(2), M(3)); cairo_curve_to(cr, M(1), M(2), M(3));
cairo_line_to(cr, M(4)); cairo_line_to(cr, M(4));
cairo_curve_to(cr, M(5), M(6), M(7)); cairo_curve_to(cr, M(5), M(6), M(7));
cairo_close_path (cr); cairo_close_path (cr);
cairo_identity_matrix (cr); cairo_identity_matrix (cr);
path = cairo_copy_path (cr); path = cairo_copy_path (cr);
cairo_destroy (cr); cairo_destroy (cr);
cairo_surface_destroy (surface); cairo_surface_destroy (surface);
return path; return path;
} }
static cairo_pattern_t * static cairo_pattern_t *
wave_mesh_create (double aspect, int alpha) wave_mesh_create (double aspect, int alpha)
{ {
cairo_pattern_t *pattern = cairo_pattern_create_mesh(); cairo_pattern_t *pattern = cairo_pattern_create_mesh();
cairo_matrix_t scale_matrix = {128./SIZE/SCALE, 0, 0, 128./SIZE/SCALE, 0, 0}; cairo_matrix_t scale_matrix = {128./SIZE/SCALE, 0, 0, 128./SIZE/SCALE, 0, 0};
cairo_pattern_set_matrix (pattern, &scale_matrix); cairo_pattern_set_matrix (pattern, &scale_matrix);
cairo_mesh_pattern_begin_patch(pattern); cairo_mesh_pattern_begin_patch(pattern);
cairo_mesh_pattern_line_to(pattern, M(0)); cairo_mesh_pattern_line_to(pattern, M(0));
cairo_mesh_pattern_curve_to(pattern, M(1), M(2), M(3)); cairo_mesh_pattern_curve_to(pattern, M(1), M(2), M(3));
cairo_mesh_pattern_line_to(pattern, M(4)); cairo_mesh_pattern_line_to(pattern, M(4));
cairo_mesh_pattern_curve_to(pattern, M(5), M(6), M(7)); cairo_mesh_pattern_curve_to(pattern, M(5), M(6), M(7));
if (alpha) if (alpha)
{ {
cairo_mesh_pattern_set_corner_color_rgba(pattern, 0, 1, 1, 1, .5); cairo_mesh_pattern_set_corner_color_rgba(pattern, 0, 1, 1, 1, .5);
cairo_mesh_pattern_set_corner_color_rgba(pattern, 1,.5,.5,.5, .5); cairo_mesh_pattern_set_corner_color_rgba(pattern, 1,.5,.5,.5, .5);
cairo_mesh_pattern_set_corner_color_rgba(pattern, 2, 0, 0, 0, .5); cairo_mesh_pattern_set_corner_color_rgba(pattern, 2, 0, 0, 0, .5);
cairo_mesh_pattern_set_corner_color_rgba(pattern, 3,.5,.5,.5, .5); cairo_mesh_pattern_set_corner_color_rgba(pattern, 3,.5,.5,.5, .5);
} }
else else
{ {
cairo_mesh_pattern_set_corner_color_rgb(pattern, 0, 0, 0, .5); cairo_mesh_pattern_set_corner_color_rgb(pattern, 0, 0, 0, .5);
cairo_mesh_pattern_set_corner_color_rgb(pattern, 1, 1, 0, .5); cairo_mesh_pattern_set_corner_color_rgb(pattern, 1, 1, 0, .5);
cairo_mesh_pattern_set_corner_color_rgb(pattern, 2, 1, 1, .5); cairo_mesh_pattern_set_corner_color_rgb(pattern, 2, 1, 1, .5);
cairo_mesh_pattern_set_corner_color_rgb(pattern, 3, 0, 1, .5); cairo_mesh_pattern_set_corner_color_rgb(pattern, 3, 0, 1, .5);
} }
cairo_mesh_pattern_end_patch(pattern); cairo_mesh_pattern_end_patch(pattern);
return pattern; return pattern;
} }
static cairo_surface_t * static cairo_surface_t *
scale_flag (cairo_surface_t *flag) scale_flag (cairo_surface_t *flag)
{ {
unsigned int w = cairo_image_surface_get_width (flag); unsigned int w = cairo_image_surface_get_width (flag);
unsigned int h = cairo_image_surface_get_height (flag); unsigned int h = cairo_image_surface_get_height (flag);
cairo_surface_t *scaled = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 256,256); cairo_surface_t *scaled = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 256,256);
cairo_t *cr = cairo_create (scaled); cairo_t *cr = cairo_create (scaled);
cairo_scale (cr, 256./w, 256./h); cairo_scale (cr, 256./w, 256./h);
cairo_set_source_surface (cr, flag, 0, 0); cairo_set_source_surface (cr, flag, 0, 0);
cairo_pattern_set_filter (cairo_get_source (cr), CAIRO_FILTER_BEST); cairo_pattern_set_filter (cairo_get_source (cr), CAIRO_FILTER_BEST);
cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_PAD); cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_PAD);
cairo_paint (cr); cairo_paint (cr);
cairo_destroy (cr); cairo_destroy (cr);
return scaled; return scaled;
} }
static cairo_surface_t * static cairo_surface_t *
load_scaled_flag (const char *filename, double *aspect) load_scaled_flag (const char *filename, double *aspect)
{ {
cairo_surface_t *flag = cairo_image_surface_create_from_png (filename); cairo_surface_t *flag = cairo_image_surface_create_from_png (filename);
cairo_surface_t *scaled = scale_flag (flag); cairo_surface_t *scaled = scale_flag (flag);
*aspect = (double) cairo_image_surface_get_width (flag) / *aspect = (double) cairo_image_surface_get_width (flag) /
(double) cairo_image_surface_get_height (flag); (double) cairo_image_surface_get_height (flag);
cairo_surface_destroy (flag); cairo_surface_destroy (flag);
return scaled; return scaled;
} }
static int static int
is_transparent (uint32_t pix) is_transparent (uint32_t pix)
{ {
return ((pix>>24) < 0xff); return ((pix>>24) < 0xff);
} }
static int static int
border_is_transparent (cairo_surface_t *scaled_flag) border_is_transparent (cairo_surface_t *scaled_flag)
{ {
/* Some flags might have a border already. As such, skip /* Some flags might have a border already. As such, skip
* a few pixels on each side... */ * a few pixels on each side... */
const unsigned int skip = 5; const unsigned int skip = 5;
uint32_t *s = (uint32_t *) cairo_image_surface_get_data (scaled_flag); uint32_t *s = (uint32_t *) cairo_image_surface_get_data (scaled_flag);
unsigned int width = cairo_image_surface_get_width (scaled_flag); unsigned int width = cairo_image_surface_get_width (scaled_flag);
unsigned int height = cairo_image_surface_get_height (scaled_flag); unsigned int height = cairo_image_surface_get_height (scaled_flag);
unsigned int sstride = cairo_image_surface_get_stride (scaled_flag) / 4; unsigned int sstride = cairo_image_surface_get_stride (scaled_flag) / 4;
int transparent = 0; int transparent = 0;
assert (width > 2 * skip && height > 2 * skip); assert (width > 2 * skip && height > 2 * skip);
for (unsigned int x = skip; x < width - skip; x++) for (unsigned int x = skip; x < width - skip; x++)
transparent |= is_transparent (s[x]); transparent |= is_transparent (s[x]);
s += sstride; s += sstride;
for (unsigned int y = 1 + skip; y < height - 1 - skip; y++) for (unsigned int y = 1 + skip; y < height - 1 - skip; y++)
{ {
transparent |= is_transparent (s[skip]); transparent |= is_transparent (s[skip]);
transparent |= is_transparent (s[width - 1 - skip]); transparent |= is_transparent (s[width - 1 - skip]);
s += sstride; s += sstride;
} }
for (unsigned int x = skip; x < width - skip; x++) for (unsigned int x = skip; x < width - skip; x++)
transparent |= is_transparent (s[x]); transparent |= is_transparent (s[x]);
return transparent; return transparent;
} }
static cairo_t * static cairo_t *
create_image (void) create_image (void)
{ {
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
(SIZE+2*MARGIN)*SCALE, (SIZE+2*MARGIN)*SCALE,
(SIZE+2*MARGIN)*SCALE); (SIZE+2*MARGIN)*SCALE);
cairo_t *cr = cairo_create (surface); cairo_t *cr = cairo_create (surface);
cairo_surface_destroy (surface); cairo_surface_destroy (surface);
return cr; return cr;
} }
static cairo_surface_t * static cairo_surface_t *
wave_surface_create (double aspect) wave_surface_create (double aspect)
{ {
cairo_t *cr = create_image (); cairo_t *cr = create_image ();
cairo_surface_t *surface = cairo_surface_reference (cairo_get_target (cr)); cairo_surface_t *surface = cairo_surface_reference (cairo_get_target (cr));
cairo_pattern_t *mesh = wave_mesh_create (aspect, 0); cairo_pattern_t *mesh = wave_mesh_create (aspect, 0);
cairo_set_source (cr, mesh); cairo_set_source (cr, mesh);
cairo_paint (cr); cairo_paint (cr);
cairo_pattern_destroy (mesh); cairo_pattern_destroy (mesh);
cairo_destroy (cr); cairo_destroy (cr);
return surface; return surface;
} }
static cairo_surface_t * static cairo_surface_t *
texture_map (cairo_surface_t *src, cairo_surface_t *tex) texture_map (cairo_surface_t *src, cairo_surface_t *tex)
{ {
uint32_t *s = (uint32_t *) cairo_image_surface_get_data (src); uint32_t *s = (uint32_t *) cairo_image_surface_get_data (src);
unsigned int width = cairo_image_surface_get_width (src); unsigned int width = cairo_image_surface_get_width (src);
unsigned int height = cairo_image_surface_get_height (src); unsigned int height = cairo_image_surface_get_height (src);
unsigned int sstride = cairo_image_surface_get_stride (src) / 4; unsigned int sstride = cairo_image_surface_get_stride (src) / 4;
cairo_surface_t *dst = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); cairo_surface_t *dst = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
uint32_t *d = (uint32_t *) cairo_image_surface_get_data (dst); uint32_t *d = (uint32_t *) cairo_image_surface_get_data (dst);
unsigned int dstride = cairo_image_surface_get_stride (dst) / 4; unsigned int dstride = cairo_image_surface_get_stride (dst) / 4;
uint32_t *t = (uint32_t *) cairo_image_surface_get_data (tex); uint32_t *t = (uint32_t *) cairo_image_surface_get_data (tex);
unsigned int twidth = cairo_image_surface_get_width (tex); unsigned int twidth = cairo_image_surface_get_width (tex);
unsigned int theight = cairo_image_surface_get_height (tex); unsigned int theight = cairo_image_surface_get_height (tex);
unsigned int tstride = cairo_image_surface_get_stride (tex) / 4; unsigned int tstride = cairo_image_surface_get_stride (tex) / 4;
assert (twidth == 256 && theight == 256); assert (twidth == 256 && theight == 256);
for (unsigned int y = 0; y < height; y++) for (unsigned int y = 0; y < height; y++)
{ {
for (unsigned int x = 0; x < width; x++) for (unsigned int x = 0; x < width; x++)
{ {
unsigned int pix = s[x]; unsigned int pix = s[x];
unsigned int sa = pix >> 24; unsigned int sa = pix >> 24;
unsigned int sr = (pix >> 16) & 0xFF; unsigned int sr = (pix >> 16) & 0xFF;
unsigned int sg = (pix >> 8) & 0xFF; unsigned int sg = (pix >> 8) & 0xFF;
unsigned int sb = (pix ) & 0xFF; unsigned int sb = (pix ) & 0xFF;
if (sa == 0) if (sa == 0)
{ {
d[x] = 0; d[x] = 0;
continue; continue;
} }
if (sa != 255) if (sa != 255)
{ {
sr = sr * 255 / sa; sr = sr * 255 / sa;
sg = sg * 255 / sa; sg = sg * 255 / sa;
sb = sb * 255 / sa; sb = sb * 255 / sa;
} }
assert (sb >= 127 && sb <= 129); assert (sb >= 127 && sb <= 129);
d[x] = t[tstride * sg + sr]; d[x] = t[tstride * sg + sr];
} }
s += sstride; s += sstride;
d += dstride; d += dstride;
} }
cairo_surface_mark_dirty (dst); cairo_surface_mark_dirty (dst);
return dst; return dst;
} }
static void static void
wave_flag (const char *filename, const char *out_prefix) wave_flag (const char *filename, const char *out_prefix)
{ {
static cairo_path_t *standard_wave_path; static cairo_path_t *standard_wave_path;
static cairo_surface_t *standard_wave_surface; static cairo_surface_t *standard_wave_surface;
cairo_path_t *wave_path; cairo_path_t *wave_path;
cairo_surface_t *wave_surface; cairo_surface_t *wave_surface;
int border_transparent; int border_transparent;
char out[1000]; char out[1000];
double aspect = 0; double aspect = 0;
cairo_surface_t *scaled_flag, *waved_flag; cairo_surface_t *scaled_flag, *waved_flag;
cairo_t *cr; cairo_t *cr;
if (debug) printf ("Processing %s\n", filename); if (debug) printf ("Processing %s\n", filename);
scaled_flag = load_scaled_flag (filename, &aspect); scaled_flag = load_scaled_flag (filename, &aspect);
aspect /= std_aspect; aspect /= std_aspect;
aspect = sqrt (aspect); // Discount the effect aspect = sqrt (aspect); // Discount the effect
if (.9 <= aspect && aspect <= 1.1) if (.9 <= aspect && aspect <= 1.1)
{ {
if (debug) printf ("Standard aspect ratio\n"); if (debug) printf ("Standard aspect ratio\n");
aspect = 1.; aspect = 1.;
} }
if (aspect == 1.) if (aspect == 1.)
{ {
if (!standard_wave_path) if (!standard_wave_path)
standard_wave_path = wave_path_create (aspect); standard_wave_path = wave_path_create (aspect);
if (!standard_wave_surface) if (!standard_wave_surface)
standard_wave_surface = wave_surface_create (aspect); standard_wave_surface = wave_surface_create (aspect);
wave_path = standard_wave_path; wave_path = standard_wave_path;
wave_surface = standard_wave_surface; wave_surface = standard_wave_surface;
} }
else else
{ {
wave_path = wave_path_create (aspect); wave_path = wave_path_create (aspect);
wave_surface = wave_surface_create (aspect); wave_surface = wave_surface_create (aspect);
} }
border_transparent = border_is_transparent (scaled_flag); border_transparent = border_is_transparent (scaled_flag);
waved_flag = texture_map (wave_surface, scaled_flag); waved_flag = texture_map (wave_surface, scaled_flag);
cairo_surface_destroy (scaled_flag); cairo_surface_destroy (scaled_flag);
cr = create_image (); cr = create_image ();
cairo_translate (cr, SCALE * MARGIN, SCALE * MARGIN); cairo_translate (cr, SCALE * MARGIN, SCALE * MARGIN);
// Paint waved flag // Paint waved flag
cairo_set_source_surface (cr, waved_flag, 0, 0); cairo_set_source_surface (cr, waved_flag, 0, 0);
cairo_append_path (cr, wave_path); cairo_append_path (cr, wave_path);
if (!debug) if (!debug)
cairo_clip_preserve (cr); cairo_clip_preserve (cr);
cairo_paint (cr); cairo_paint (cr);
// Paint border // Paint border
if (!border_transparent) if (!border_transparent)
{ {
double border_alpha = .2; double border_alpha = .2;
double border_width = 4 * SCALE; double border_width = 4 * SCALE;
double border_gray = 0x42/255.; double border_gray = 0x42/255.;
if (debug) if (debug)
printf ("Border: alpha %g width %g gray %g\n", printf ("Border: alpha %g width %g gray %g\n",
border_alpha, border_width/SCALE, border_gray); border_alpha, border_width/SCALE, border_gray);
cairo_save (cr); cairo_save (cr);
cairo_set_source_rgba (cr, cairo_set_source_rgba (cr,
border_gray * border_alpha, border_gray * border_alpha,
border_gray * border_alpha, border_gray * border_alpha,
border_gray * border_alpha, border_gray * border_alpha,
border_alpha); border_alpha);
cairo_set_line_width (cr, 2*border_width); cairo_set_line_width (cr, 2*border_width);
if (!debug) if (!debug)
cairo_set_operator (cr, CAIRO_OPERATOR_MULTIPLY); cairo_set_operator (cr, CAIRO_OPERATOR_MULTIPLY);
cairo_stroke (cr); cairo_stroke (cr);
cairo_restore (cr); cairo_restore (cr);
} }
else else
{ {
if (debug) printf ("Transparent border\n"); if (debug) printf ("Transparent border\n");
cairo_new_path (cr); cairo_new_path (cr);
} }
// Paint shade gradient // Paint shade gradient
{ {
cairo_pattern_t *gradient = wave_mesh_create (aspect, 1); cairo_pattern_t *gradient = wave_mesh_create (aspect, 1);
cairo_pattern_t *w = cairo_pattern_create_for_surface (waved_flag); cairo_pattern_t *w = cairo_pattern_create_for_surface (waved_flag);
cairo_save (cr); cairo_save (cr);
cairo_set_source (cr, gradient); cairo_set_source (cr, gradient);
cairo_set_operator (cr, CAIRO_OPERATOR_SOFT_LIGHT); cairo_set_operator (cr, CAIRO_OPERATOR_SOFT_LIGHT);
cairo_mask (cr, w); cairo_mask (cr, w);
cairo_restore (cr); cairo_restore (cr);
cairo_pattern_destroy (w); cairo_pattern_destroy (w);
} }
if (debug) if (debug)
{ {
/* Draw mesh points. */ /* Draw mesh points. */
cairo_save (cr); cairo_save (cr);
cairo_scale (cr, SIZE/128.*SCALE, SIZE/128.*SCALE); cairo_scale (cr, SIZE/128.*SCALE, SIZE/128.*SCALE);
cairo_set_source_rgba (cr, .5,.0,.0,.9); cairo_set_source_rgba (cr, .5,.0,.0,.9);
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
for (unsigned int i = 0; i < sizeof (mesh_points) / sizeof (mesh_points[0]); i++) for (unsigned int i = 0; i < sizeof (mesh_points) / sizeof (mesh_points[0]); i++)
{ {
cairo_move_to (cr, M(i)); cairo_move_to (cr, M(i));
cairo_rel_line_to (cr, 0, 0); cairo_rel_line_to (cr, 0, 0);
} }
cairo_set_line_width (cr, 2); cairo_set_line_width (cr, 2);
cairo_stroke (cr); cairo_stroke (cr);
for (unsigned int i = 0; i < 4; i++) for (unsigned int i = 0; i < 4; i++)
{ {
cairo_move_to (cr, M(2*i)); cairo_move_to (cr, M(2*i));
cairo_line_to (cr, M(2*i+1)); cairo_line_to (cr, M(2*i+1));
cairo_move_to (cr, M(2*i)); cairo_move_to (cr, M(2*i));
cairo_line_to (cr, M(7 - 2*i)); cairo_line_to (cr, M(7 - 2*i));
} }
cairo_set_line_width (cr, .5); cairo_set_line_width (cr, .5);
cairo_stroke (cr); cairo_stroke (cr);
cairo_restore (cr); cairo_restore (cr);
} }
if (!debug) if (!debug)
{ {
/* Scale down, 2x at a time, to get best downscaling, because cairo's /* Scale down, 2x at a time, to get best downscaling, because cairo's
* downscaling is crap... :( */ * downscaling is crap... :( */
unsigned int scale = SCALE; unsigned int scale = SCALE;
while (scale > 1) while (scale > 1)
{ {
cairo_surface_t *old_surface, *new_surface; cairo_surface_t *old_surface, *new_surface;
old_surface = cairo_surface_reference (cairo_get_target (cr)); old_surface = cairo_surface_reference (cairo_get_target (cr));
assert (scale % 2 == 0); assert (scale % 2 == 0);
scale /= 2; scale /= 2;
cairo_destroy (cr); cairo_destroy (cr);
new_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, (SIZE+2*MARGIN)*scale, (SIZE+2*MARGIN)*scale); new_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, (SIZE+2*MARGIN)*scale, (SIZE+2*MARGIN)*scale);
cr = cairo_create (new_surface); cr = cairo_create (new_surface);
cairo_scale (cr, .5, .5); cairo_scale (cr, .5, .5);
cairo_set_source_surface (cr, old_surface, 0, 0); cairo_set_source_surface (cr, old_surface, 0, 0);
cairo_paint (cr); cairo_paint (cr);
cairo_surface_destroy (old_surface); cairo_surface_destroy (old_surface);
cairo_surface_destroy (new_surface); cairo_surface_destroy (new_surface);
} }
} }
*out = '\0'; *out = '\0';
strcat (out, out_prefix); strcat (out, out_prefix);
// diff from upstream. we call this a bit differently, filename might not be in cwd. // diff from upstream. we call this a bit differently, filename might not be in cwd.
// basename wants a non-const argument. The problem here is paths that end in a // basename wants a non-const argument. The problem here is paths that end in a
// slash, POSIX basename removes them while GNU just returns a pointer to that // slash, POSIX basename removes them while GNU just returns a pointer to that
// slash. Since this is supposed to be a filename such input is illegal for us. // slash. Since this is supposed to be a filename such input is illegal for us.
// We're already not checking for overflow of the output buffer anyway... // We're already not checking for overflow of the output buffer anyway...
strcat (out, basename((char *) filename)); strcat (out, basename((char *) filename));
cairo_surface_write_to_png (cairo_get_target (cr), out); cairo_surface_write_to_png (cairo_get_target (cr), out);
cairo_destroy (cr); cairo_destroy (cr);
if (wave_path != standard_wave_path) if (wave_path != standard_wave_path)
cairo_path_destroy (wave_path); cairo_path_destroy (wave_path);
if (wave_surface != standard_wave_surface) if (wave_surface != standard_wave_surface)
cairo_surface_destroy (wave_surface); cairo_surface_destroy (wave_surface);
} }
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
const char *out_prefix; const char *out_prefix;
if (argc < 3) if (argc < 3)
{ {
fprintf (stderr, "Usage: waveflag [-debug] out-prefix [in.png]...\n"); fprintf (stderr, "Usage: waveflag [-debug] out-prefix [in.png]...\n");
return 1; return 1;
} }
if (!strcmp (argv[1], "-debug")) if (!strcmp (argv[1], "-debug"))
{ {
debug = 1; debug = 1;
argc--, argv++; argc--, argv++;
} }
out_prefix = argv[1]; out_prefix = argv[1];
argc--, argv++; argc--, argv++;
for (argc--, argv++; argc; argc--, argv++) for (argc--, argv++; argc; argc--, argv++)
wave_flag (*argv, out_prefix); wave_flag (*argv, out_prefix);
return 0; return 0;
} }