blob: b5ed1b52a19d970f8fd65a2450cdc145234a058e [file] [log] [blame]
Roozbeh Pournader0e969e22016-03-09 23:08:45 -08001#!/usr/bin/env python
2
3import collections
4import glob
5from os import path
6import sys
7from xml.etree import ElementTree
8
9from fontTools import ttLib
10
11LANG_TO_SCRIPT = {
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070012 'as': 'Beng',
13 'bn': 'Beng',
14 'cy': 'Latn',
15 'da': 'Latn',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080016 'de': 'Latn',
17 'en': 'Latn',
18 'es': 'Latn',
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070019 'et': 'Latn',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080020 'eu': 'Latn',
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070021 'fr': 'Latn',
22 'ga': 'Latn',
23 'gu': 'Gujr',
24 'hi': 'Deva',
25 'hr': 'Latn',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080026 'hu': 'Latn',
27 'hy': 'Armn',
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070028 'ja': 'Jpan',
29 'kn': 'Knda',
30 'ko': 'Kore',
31 'ml': 'Mlym',
32 'mn': 'Cyrl',
33 'mr': 'Deva',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080034 'nb': 'Latn',
35 'nn': 'Latn',
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070036 'or': 'Orya',
37 'pa': 'Guru',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080038 'pt': 'Latn',
Jungshik Shin6c4f9e02016-03-19 09:32:34 -070039 'sl': 'Latn',
40 'ta': 'Taml',
41 'te': 'Telu',
42 'tk': 'Latn',
Roozbeh Pournader0e969e22016-03-09 23:08:45 -080043}
44
45def lang_to_script(lang_code):
46 lang = lang_code.lower()
47 while lang not in LANG_TO_SCRIPT:
48 hyphen_idx = lang.rfind('-')
49 assert hyphen_idx != -1, (
50 'We do not know what script the "%s" language is written in.'
51 % lang_code)
52 assumed_script = lang[hyphen_idx+1:]
53 if len(assumed_script) == 4 and assumed_script.isalpha():
54 # This is actually the script
55 return assumed_script.title()
56 lang = lang[:hyphen_idx]
57 return LANG_TO_SCRIPT[lang]
58
59
60def get_best_cmap(font):
61 font_file, index = font
62 font_path = path.join(_fonts_dir, font_file)
63 if index is not None:
64 ttfont = ttLib.TTFont(font_path, fontNumber=index)
65 else:
66 ttfont = ttLib.TTFont(font_path)
67 all_unicode_cmap = None
68 bmp_cmap = None
69 for cmap in ttfont['cmap'].tables:
70 specifier = (cmap.format, cmap.platformID, cmap.platEncID)
71 if specifier == (4, 3, 1):
72 assert bmp_cmap is None, 'More than one BMP cmap in %s' % (font, )
73 bmp_cmap = cmap
74 elif specifier == (12, 3, 10):
75 assert all_unicode_cmap is None, (
76 'More than one UCS-4 cmap in %s' % (font, ))
77 all_unicode_cmap = cmap
78
79 return all_unicode_cmap.cmap if all_unicode_cmap else bmp_cmap.cmap
80
81
82def assert_font_supports_any_of_chars(font, chars):
83 best_cmap = get_best_cmap(font)
84 for char in chars:
85 if char in best_cmap:
86 return
87 sys.exit('None of characters in %s were found in %s' % (chars, font))
88
89
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -070090def assert_font_supports_all_of_chars(font, chars):
91 best_cmap = get_best_cmap(font)
92 for char in chars:
93 assert char in best_cmap, (
94 'U+%04X was not found in %s' % (char, font))
95
96
97def assert_font_supports_none_of_chars(font, chars):
98 best_cmap = get_best_cmap(font)
99 for char in chars:
100 assert char not in best_cmap, (
101 'U+%04X was found in %s' % (char, font))
102
103
Roozbeh Pournader0e969e22016-03-09 23:08:45 -0800104def check_hyphens(hyphens_dir):
105 # Find all the scripts that need automatic hyphenation
106 scripts = set()
107 for hyb_file in glob.iglob(path.join(hyphens_dir, '*.hyb')):
108 hyb_file = path.basename(hyb_file)
109 assert hyb_file.startswith('hyph-'), (
110 'Unknown hyphenation file %s' % hyb_file)
111 lang_code = hyb_file[hyb_file.index('-')+1:hyb_file.index('.')]
112 scripts.add(lang_to_script(lang_code))
113
114 HYPHENS = {0x002D, 0x2010}
115 for script in scripts:
116 fonts = _script_to_font_map[script]
117 assert fonts, 'No fonts found for the "%s" script' % script
118 for font in fonts:
119 assert_font_supports_any_of_chars(font, HYPHENS)
120
121
122def parse_fonts_xml(fonts_xml_path):
123 global _script_to_font_map, _fallback_chain
124 _script_to_font_map = collections.defaultdict(set)
125 _fallback_chain = []
126 tree = ElementTree.parse(fonts_xml_path)
127 for family in tree.findall('family'):
128 name = family.get('name')
129 variant = family.get('variant')
130 langs = family.get('lang')
131 if name:
132 assert variant is None, (
133 'No variant expected for LGC font %s.' % name)
134 assert langs is None, (
135 'No language expected for LGC fonts %s.' % name)
136 else:
137 assert variant in {None, 'elegant', 'compact'}, (
138 'Unexpected value for variant: %s' % variant)
139
140 if langs:
141 langs = langs.split()
142 scripts = {lang_to_script(lang) for lang in langs}
143 else:
144 scripts = set()
145
146 for child in family:
147 assert child.tag == 'font', (
148 'Unknown tag <%s>' % child.tag)
149 font_file = child.text
150 weight = int(child.get('weight'))
151 assert weight % 100 == 0, (
152 'Font weight "%d" is not a multiple of 100.' % weight)
153
154 style = child.get('style')
155 assert style in {'normal', 'italic'}, (
156 'Unknown style "%s"' % style)
157
158 index = child.get('index')
159 if index:
160 index = int(index)
161
162 _fallback_chain.append((
163 name,
164 frozenset(scripts),
165 variant,
166 weight,
167 style,
168 (font_file, index)))
169
170 if name: # non-empty names are used for default LGC fonts
171 map_scripts = {'Latn', 'Grek', 'Cyrl'}
172 else:
173 map_scripts = scripts
174 for script in map_scripts:
175 _script_to_font_map[script].add((font_file, index))
176
177
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700178def check_emoji_availability():
179 emoji_fonts = [font[5] for font in _fallback_chain if 'Zsye' in font[1]]
Roozbeh Pournader27ec3ac2016-03-31 13:05:32 -0700180 assert len(emoji_fonts) == 1, 'There are %d emoji fonts.' % len(emoji_fonts)
181 emoji_font = emoji_fonts[0]
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700182 emoji_chars = _emoji_properties['Emoji']
Roozbeh Pournader27ec3ac2016-03-31 13:05:32 -0700183 assert_font_supports_all_of_chars(emoji_font, emoji_chars)
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700184
185
186def check_emoji_defaults():
187 default_emoji_chars = _emoji_properties['Emoji_Presentation']
Roozbeh Pournader7b822e52016-03-16 18:55:32 -0700188 missing_text_chars = _emoji_properties['Emoji'] - default_emoji_chars
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700189 emoji_font_seen = False
190 for name, scripts, variant, weight, style, font in _fallback_chain:
191 if 'Zsye' in scripts:
192 emoji_font_seen = True
193 # No need to check the emoji font
194 continue
195 # For later fonts, we only check them if they have a script
196 # defined, since the defined script may get them to a higher
197 # score even if they appear after the emoji font.
198 if emoji_font_seen and not scripts:
199 continue
200
Roozbeh Pournader7b822e52016-03-16 18:55:32 -0700201 # Check default emoji-style characters
202 assert_font_supports_none_of_chars(font, sorted(default_emoji_chars))
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700203
Roozbeh Pournader7b822e52016-03-16 18:55:32 -0700204 # Mark default text-style characters appearing in fonts above the emoji
205 # font as seen
206 if not emoji_font_seen:
207 missing_text_chars -= set(get_best_cmap(font))
208
209 # Noto does not have monochrome symbols for Unicode 7.0 wingdings and
210 # webdings
211 missing_text_chars -= _chars_by_age['7.0']
212 # TODO: Remove these after b/26113320 is fixed
213 missing_text_chars -= {
214 0x263A, # WHITE SMILING FACE
215 0x270C, # VICTORY HAND
216 0x2744, # SNOWFLAKE
217 0x2764, # HEAVY BLACK HEART
218 }
219 assert missing_text_chars == set(), (
220 'Text style version of some emoji characters are missing.')
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700221
222
Roozbeh Pournader7b822e52016-03-16 18:55:32 -0700223# Setting reverse to true returns a dictionary that maps the values to sets of
224# characters, useful for some binary properties. Otherwise, we get a
225# dictionary that maps characters to the property values, assuming there's only
226# one property in the file.
227def parse_unicode_datafile(file_path, reverse=False):
228 if reverse:
229 output_dict = collections.defaultdict(set)
230 else:
231 output_dict = {}
232 with open(file_path) as datafile:
233 for line in datafile:
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700234 if '#' in line:
235 line = line[:line.index('#')]
236 line = line.strip()
237 if not line:
238 continue
239 char_range, prop = line.split(';')
240 char_range = char_range.strip()
241 prop = prop.strip()
242 if '..' in char_range:
243 char_start, char_end = char_range.split('..')
244 else:
245 char_start = char_end = char_range
246 char_start = int(char_start, 16)
247 char_end = int(char_end, 16)
Roozbeh Pournader7b822e52016-03-16 18:55:32 -0700248 char_range = xrange(char_start, char_end+1)
249 if reverse:
250 output_dict[prop].update(char_range)
251 else:
252 for char in char_range:
253 assert char not in output_dict
254 output_dict[char] = prop
255 return output_dict
256
257
258def parse_ucd(ucd_path):
259 global _emoji_properties, _chars_by_age
260 _emoji_properties = parse_unicode_datafile(
261 path.join(ucd_path, 'emoji-data.txt'), reverse=True)
262 _chars_by_age = parse_unicode_datafile(
263 path.join(ucd_path, 'DerivedAge.txt'), reverse=True)
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700264
265
Roozbeh Pournader0e969e22016-03-09 23:08:45 -0800266def main():
267 target_out = sys.argv[1]
268 global _fonts_dir
269 _fonts_dir = path.join(target_out, 'fonts')
270
271 fonts_xml_path = path.join(target_out, 'etc', 'fonts.xml')
272 parse_fonts_xml(fonts_xml_path)
273
274 hyphens_dir = path.join(target_out, 'usr', 'hyphen-data')
275 check_hyphens(hyphens_dir)
276
Roozbeh Pournader27ec3ac2016-03-31 13:05:32 -0700277 check_emoji = sys.argv[2]
278 if check_emoji == 'true':
279 ucd_path = sys.argv[3]
280 parse_ucd(ucd_path)
281 check_emoji_availability()
282 check_emoji_defaults()
Roozbeh Pournaderfa1facc2016-03-16 13:53:47 -0700283
Roozbeh Pournader0e969e22016-03-09 23:08:45 -0800284
285if __name__ == '__main__':
286 main()