mirror of
https://github.com/googlefonts/noto-emoji.git
synced 2026-08-14 05:21:45 +00:00
Upgrade to Python 3
This commit is contained in:
parent
eea78738cd
commit
0597775dd7
30 changed files with 5599 additions and 5488 deletions
13
Makefile
13
Makefile
|
|
@ -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"
|
||||||
|
|
|
||||||
448
add_aliases.py
448
add_aliases.py
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
812
add_glyphs.py
812
add_glyphs.py
|
|
@ -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()
|
|
||||||
|
|
|
||||||
|
|
@ -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:])
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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()
|
||||||
|
|
|
||||||
170
flag_info.py
170
flag_info.py
|
|
@ -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.
384
gen_version.py
384
gen_version.py
|
|
@ -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
|
|
@ -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 Worker’s Helmet',
|
(0x26d1,): 'Rescue Worker’s 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,): 'Woman’s Hat',
|
(0x1f452,): 'Woman’s Hat',
|
||||||
(0x1f45a,): 'Woman’s Clothes',
|
(0x1f45a,): 'Woman’s Clothes',
|
||||||
(0x1f45e,): 'Man’s Shoe',
|
(0x1f45e,): 'Man’s Shoe',
|
||||||
(0x1f461,): 'Woman’s Sandal',
|
(0x1f461,): 'Woman’s Sandal',
|
||||||
(0x1f462,): 'Woman’s Boot',
|
(0x1f462,): 'Woman’s 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,): 'Men’s Room',
|
(0x1f6b9,): 'Men’s Room',
|
||||||
(0x1f6ba,): 'Women’s Room',
|
(0x1f6ba,): 'Women’s 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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 26 and later.
|
View using Firefox 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:])
|
||||||
|
|
|
||||||
146
map_pua_emoji.py
146
map_pua_emoji.py
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
396
svg_builder.py
396
svg_builder.py
|
|
@ -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)
|
||||||
|
|
|
||||||
695
svg_cleaner.py
695
svg_cleaner.py
|
|
@ -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()
|
||||||
|
|
|
||||||
26
third_party/color_emoji/LICENSE
vendored
26
third_party/color_emoji/LICENSE
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
26
third_party/color_emoji/README
vendored
26
third_party/color_emoji/README
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
22
third_party/color_emoji/README.third_party
vendored
22
third_party/color_emoji/README.third_party
vendored
|
|
@ -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.
|
||||||
|
|
|
||||||
1129
third_party/color_emoji/emoji_builder.py
vendored
1129
third_party/color_emoji/emoji_builder.py
vendored
File diff suppressed because it is too large
Load diff
223
third_party/color_emoji/png.py
vendored
223
third_party/color_emoji/png.py
vendored
|
|
@ -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)
|
||||||
|
|
|
||||||
902
waveflag.c
902
waveflag.c
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue