blob: 8a733d2796ecb3599fc4c6ec8dadefa35ea281ed [file] [log] [blame]
Yu-Ping Wud71b4452020-06-16 11:00:26 +08001#!/usr/bin/env python
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08005"""Script to generate bitmaps for firmware screens."""
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08006
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08007import argparse
8from collections import defaultdict, namedtuple
9import copy
Yu-Ping Wue445e042020-11-19 15:53:42 +080010import fractions
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080011import glob
Jes Klinke1687a992020-06-16 13:47:17 -070012import json
Hung-Te Lin04addcc2015-03-23 18:43:30 +080013import multiprocessing
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080014import os
15import re
Jes Klinke1687a992020-06-16 13:47:17 -070016import shutil
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080017import signal
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080018import subprocess
19import sys
Jes Klinke1687a992020-06-16 13:47:17 -070020import tempfile
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080021from xml.etree import ElementTree
Hung-Te Lin04addcc2015-03-23 18:43:30 +080022
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080023import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080024from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080025
26SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080027
28STRINGS_GRD_FILE = 'firmware_strings.grd'
29STRINGS_JSON_FILE_TMPL = '{}.json'
30FORMAT_FILE = 'format.yaml'
31BOARDS_CONFIG_FILE = 'boards.yaml'
32
33TXT_TO_PNG_SVG = os.path.join(SCRIPT_BASE, 'text_to_png_svg')
34STRINGS_DIR = os.path.join(SCRIPT_BASE, 'strings')
35LOCALE_DIR = os.path.join(STRINGS_DIR, 'locale')
36OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
37STAGE_DIR = os.path.join(OUTPUT_DIR, '.stage')
38STAGE_LOCALE_DIR = os.path.join(STAGE_DIR, 'locale')
39STAGE_FONT_DIR = os.path.join(STAGE_DIR, 'font')
40
41ONE_LINE_DIR = 'one_line'
42SVG_FILES = '*.svg'
43PNG_FILES = '*.png'
44
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080045DIAGNOSTIC_UI = os.getenv('DIAGNOSTIC_UI') == '1'
46
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080047# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080048KEY_DEFAULT = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080049KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080050KEY_GENERIC_FILES = 'generic_files'
51KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080052KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
53KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080054KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080055KEY_BGCOLOR = 'bgcolor'
56KEY_FGCOLOR = 'fgcolor'
57KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080058KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080059KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080060
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080061# Board config YAML key names.
62SCREEN_KEY = 'screen'
63PANEL_KEY = 'panel'
64SDCARD_KEY = 'sdcard'
65BAD_USB3_KEY = 'bad_usb3'
Yu-Ping Wue66a7b02020-11-19 15:18:08 +080066DPI_KEY = 'dpi'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080067LOCALES_KEY = 'locales'
68RTL_KEY = 'rtl'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080069RW_OVERRIDE_KEY = 'rw_override'
70
71BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080072
Jes Klinke1687a992020-06-16 13:47:17 -070073# Regular expressions used to eliminate spurious spaces and newlines in
74# translation strings.
75NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
76NEWLINE_REPLACEMENT = r'\1 \2'
77CRLF_PATTERN = re.compile(r'\r\n')
78MULTIBLANK_PATTERN = re.compile(r' *')
79
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +080080GLYPH_FONT = 'Cousine'
Yu-Ping Wu11027f02020-10-14 17:35:42 +080081
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080082LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080083
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080084
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080085class DataError(Exception):
86 pass
87
88
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080089class BuildImageError(Exception):
90 """The exception class for all errors generated during build image process."""
91
92
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080093def get_config_with_defaults(configs, key):
94 """Gets config of `key` from `configs`.
95
96 If `key` is not present in `configs`, the default config will be returned.
97 Similarly, if some config values are missing for `key`, the default ones will
98 be used.
99 """
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800100 config = configs[KEY_DEFAULT].copy()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800101 config.update(configs.get(key, {}))
102 return config
103
104
Yu-Ping Wued95df32020-11-04 17:08:15 +0800105def convert_text_to_png(locale, input_file, font, output_dir, height=None,
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800106 max_width=None, dpi=None, bgcolor='#000000',
107 fgcolor='#ffffff',
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800108 **options):
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800109 """Converts text files into PNG image files.
110
111 Args:
112 locale: Locale (language) to select implicit rendering options. None for
113 locale-independent strings.
114 input_file: Path of input text file.
115 font: Font spec.
Yu-Ping Wued95df32020-11-04 17:08:15 +0800116 height: Height.
117 max_width: Maximum width.
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800118 output_dir: Directory to generate image files.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800119 bgcolor: Background color (#rrggbb).
120 fgcolor: Foreground color (#rrggbb).
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800121 **options: Other options to be added.
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800122 """
123 name, _ = os.path.splitext(os.path.basename(input_file))
124 command = [TXT_TO_PNG_SVG, '--outdir=%s' % output_dir]
125 if locale:
126 command.append('--lan=%s' % locale)
127 if font:
128 command.append("--font='%s'" % font)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800129 if height:
130 # Font size should be proportional to the height. Here we use 2 as the
131 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
132 # will be roughly equivalent to setting the screen resolution to 1366x768.
133 font_size = height / 2
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800134 command.append('--point=%r' % font_size)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800135 if max_width:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800136 # When converting text to PNG by pango-view, the ratio of image height to
137 # the font size is usually no more than 1.1875 (with Roboto). Therefore,
138 # set the `max_width_pt` as follows to prevent UI drawing from exceeding
139 # the canvas boundary in depthcharge runtime. The divisor 2 is the same in
140 # the calculation of `font_size` above.
141 max_width_pt = int(max_width / 2 * 1.1875)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800142 command.append('--width=%d' % max_width_pt)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800143 if dpi:
144 command.append('--dpi=%d' % dpi)
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800145 command.append('--margin=0')
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800146 command.append('--bgcolor="%s"' % bgcolor)
147 command.append('--color="%s"' % fgcolor)
148
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800149 for k, v in options.items():
150 command.append('--%s="%s"' % (k, v))
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800151 command.append(input_file)
152
153 return subprocess.call(' '.join(command), shell=True,
154 stdout=subprocess.PIPE) == 0
155
156
157def convert_glyphs():
158 """Converts glyphs of ascii characters."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800159 os.makedirs(STAGE_FONT_DIR, exist_ok=True)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800160 # Remove the extra whitespace at the top/bottom within the glyphs
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800161 for c in range(ord(' '), ord('~') + 1):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800162 txt_file = os.path.join(STAGE_FONT_DIR, f'idx{c:03d}_{c:02x}.txt')
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800163 with open(txt_file, 'w', encoding='ascii') as f:
164 f.write(chr(c))
165 f.write('\n')
166 # TODO(b/163109632): Parallelize the conversion of glyphs
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800167 convert_text_to_png(None, txt_file, GLYPH_FONT, STAGE_FONT_DIR)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800168
169
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800170def _load_locale_json_file(locale, json_dir):
Jes Klinke1687a992020-06-16 13:47:17 -0700171 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800172 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800173 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700174 for tag, msgdict in json.load(input_file).items():
175 msgtext = msgdict['message']
176 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
177 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
178 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
179 # Strip any trailing whitespace. A trailing newline appears to make
180 # Pango report a larger layout size than what's actually visible.
181 msgtext = msgtext.strip()
182 result[tag] = msgtext
183 return result
184
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800185
186def parse_locale_json_file(locale, json_dir):
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800187 """Parses given firmware string json file.
Mathew King89d48c62019-02-15 10:08:39 -0700188
189 Args:
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800190 locale: The name of the locale, e.g. "da" or "pt-BR".
Jes Klinke1687a992020-06-16 13:47:17 -0700191 json_dir: Directory containing json output from grit.
Mathew King89d48c62019-02-15 10:08:39 -0700192
193 Returns:
194 A dictionary for mapping of "name to content" for files to be generated.
195 """
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800196 result = _load_locale_json_file(locale, json_dir)
197 original = _load_locale_json_file('en', json_dir)
198 for tag in original:
199 if tag not in result:
200 # Use original English text, in case translation is not yet available
201 print('WARNING: locale "%s", missing entry %s' % (locale, tag))
202 result[tag] = original[tag]
Mathew King89d48c62019-02-15 10:08:39 -0700203
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800204 return result
Mathew King89d48c62019-02-15 10:08:39 -0700205
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800206
207def parse_locale_input_files(locale, json_dir):
208 """Parses all firmware string files for the given locale.
209
210 Args:
211 locale: The name of the locale, e.g. "da" or "pt-BR".
212 json_dir: Directory containing json output from grit.
213
214 Returns:
215 A dictionary for mapping of "name to content" for files to be generated.
216 """
217 result = parse_locale_json_file(locale, json_dir)
218
219 # Walk locale directory to add pre-generated texts such as language names.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800220 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Mathew King89d48c62019-02-15 10:08:39 -0700221 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800222 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700223 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700224
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800225 return result
226
227
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800228def build_text_files(inputs, files, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800229 """Builds text files from given input data.
230
231 Args:
232 inputs: Dictionary of contents for given file name.
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800233 files: List of files.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800234 output_dir: Directory to generate text files.
235 """
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800236 for name in files:
237 file_name = os.path.join(output_dir, name + '.txt')
238 with open(file_name, 'w', encoding='utf-8-sig') as f:
239 f.write(inputs[name] + '\n')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800240
241
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800242def convert_localized_strings(formats, dpi):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800243 """Converts localized strings."""
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800244 # Make a copy of formats to avoid modifying it
245 formats = copy.deepcopy(formats)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800246
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800247 env_locales = os.getenv('LOCALES')
248 if env_locales:
249 locales = env_locales.split()
250 else:
251 locales = formats[KEY_LOCALES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800252
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800253 files = formats[KEY_LOCALIZED_FILES]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800254 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800255 files.update(formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800256
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800257 styles = formats[KEY_STYLES]
258 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800259 default_font = fonts[KEY_DEFAULT]
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800260
Yu-Ping Wu51940352020-09-17 08:48:55 +0800261 # Sources are one .grd file with identifiers chosen by engineers and
262 # corresponding English texts, as well as a set of .xlt files (one for each
263 # language other than US english) with a mapping from hash to translation.
264 # Because the keys in the xlt files are a hash of the English source text,
265 # rather than our identifiers, such as "btn_cancel", we use the "grit"
266 # command line tool to process the .grd and .xlt files, producing a set of
267 # .json files mapping our identifier to the translated string, one for every
268 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700269
Yu-Ping Wu51940352020-09-17 08:48:55 +0800270 # Create a temporary directory to place the translation output from grit in.
271 json_dir = tempfile.mkdtemp()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800272
Yu-Ping Wu51940352020-09-17 08:48:55 +0800273 # This invokes the grit build command to generate JSON files from the XTB
274 # files containing translations. The results are placed in `json_dir` as
275 # specified in firmware_strings.grd, i.e. one JSON file per locale.
276 subprocess.check_call([
277 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800278 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800279 'build',
280 '-o', os.path.join(json_dir)
281 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700282
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800283 # Ignore SIGINT in child processes
284 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800285 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800286 signal.signal(signal.SIGINT, sigint_handler)
287
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800288 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800289 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800290 print(locale, end=' ', flush=True)
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800291 inputs = parse_locale_input_files(locale, json_dir)
292 output_dir = os.path.normpath(os.path.join(STAGE_DIR, 'locale', locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800293 if not os.path.exists(output_dir):
294 os.makedirs(output_dir)
Matt Delco4c5580d2019-03-07 14:00:28 -0800295
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800296 build_text_files(inputs, files, output_dir)
Shelley Chen2f616ac2017-05-22 13:19:40 -0700297
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800298 for name, category in files.items():
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800299 style = get_config_with_defaults(styles, category)
300 args = (
301 locale,
302 os.path.join(output_dir, '%s.txt' % name),
303 fonts.get(locale, default_font),
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800304 output_dir,
305 )
306 kwargs = {
Yu-Ping Wued95df32020-11-04 17:08:15 +0800307 'height': style[KEY_HEIGHT],
308 'max_width': style[KEY_MAX_WIDTH],
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800309 'dpi': dpi,
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800310 'bgcolor': style[KEY_BGCOLOR],
311 'fgcolor': style[KEY_FGCOLOR],
312 }
313 results.append(pool.apply_async(convert_text_to_png, args, kwargs))
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800314 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700315 if json_dir is not None:
316 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800317 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800318
319 try:
320 success = [r.get() for r in results]
321 except KeyboardInterrupt:
322 pool.terminate()
323 pool.join()
324 exit('Aborted by user')
325 else:
326 pool.join()
327 if not all(success):
328 exit('Failed to render some locales')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800329
330
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800331def build_strings(formats, board_config):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800332 """Builds text strings."""
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800333 dpi = board_config[DPI_KEY]
334
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800335 # Convert glyphs
336 print('Converting glyphs...')
337 convert_glyphs()
338
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800339 # Convert generic (locale-independent) strings
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800340 files = formats[KEY_GENERIC_FILES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800341 styles = formats[KEY_STYLES]
342 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800343 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800344
345 for input_file in glob.glob(os.path.join(STRINGS_DIR, '*.txt')):
346 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800347 category = files[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800348 style = get_config_with_defaults(styles, category)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800349 if not convert_text_to_png(None, input_file, default_font, STAGE_DIR,
350 height=style[KEY_HEIGHT],
351 max_width=style[KEY_MAX_WIDTH],
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800352 dpi=dpi,
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800353 bgcolor=style[KEY_BGCOLOR],
354 fgcolor=style[KEY_FGCOLOR]):
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800355 exit('Failed to convert text %s' % input_file)
356
357 # Convert localized strings
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800358 convert_localized_strings(formats, dpi)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800359
360
361def load_boards_config(filename):
362 """Loads the configuration of all boards from `filename`.
363
364 Args:
365 filename: File name of a YAML config file.
366
367 Returns:
368 A dictionary mapping each board name to its config.
369 """
370 with open(filename, 'rb') as file:
371 raw = yaml.load(file)
372
373 configs = {}
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800374 default = raw[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800375 if not default:
376 raise BuildImageError('Default configuration is not found')
377 for boards, params in raw.items():
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800378 if boards == KEY_DEFAULT:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800379 continue
380 config = copy.deepcopy(default)
381 if params:
382 config.update(params)
383 for board in boards.replace(',', ' ').split():
384 configs[board] = config
385
386 return configs
387
388
389class Converter(object):
390 """Converter from assets, texts, URLs, and fonts to bitmap images.
391
392 Attributes:
393 ASSET_DIR (str): Directory of image assets.
394 DEFAULT_OUTPUT_EXT (str): Default output file extension.
395 DEFAULT_REPLACE_MAP (dict): Default mapping of file replacement. For
396 {'a': 'b'}, "a.*" will be converted to "b.*".
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800397 SCALE_BASE (int): The base for bitmap scales, same as UI_SCALE in
398 depthcharge. For example, if `SCALE_BASE` is 1000, then height = 200 means
399 20% of the screen height. Also see the 'styles' section in format.yaml.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800400 DEFAULT_FONT_HEIGHT (tuple): Height of the font images.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800401 ASSET_MAX_COLORS (int): Maximum colors to use for converting image assets
402 to bitmaps.
403 DEFAULT_BACKGROUND (tuple): Default background color.
404 BACKGROUND_COLORS (dict): Background color of each image. Key is the image
405 name and value is a tuple of RGB values.
406 """
407
408 ASSET_DIR = 'assets'
409 DEFAULT_OUTPUT_EXT = '.bmp'
410
411 DEFAULT_REPLACE_MAP = {
412 'rec_sel_desc1_no_sd': '',
413 'rec_sel_desc1_no_phone_no_sd': '',
414 'rec_disk_step1_desc0_no_sd': '',
415 'rec_to_dev_desc1_phyrec': '',
416 'rec_to_dev_desc1_power': '',
417 'navigate0_tablet': '',
418 'navigate1_tablet': '',
419 'nav-button_power': '',
420 'nav-button_volume_up': '',
421 'nav-button_volume_down': '',
422 'broken_desc_phyrec': '',
423 'broken_desc_detach': '',
424 }
425
426 # scales
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800427 SCALE_BASE = 1000
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800428 DEFAULT_FONT_HEIGHT = 20
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800429
430 # background colors
431 DEFAULT_BACKGROUND = (0x20, 0x21, 0x24)
432 LANG_HEADER_BACKGROUND = (0x16, 0x17, 0x19)
433 LINK_SELECTED_BACKGROUND = (0x2a, 0x2f, 0x39)
434 ASSET_MAX_COLORS = 128
435
436 BACKGROUND_COLORS = {
437 'ic_dropdown': LANG_HEADER_BACKGROUND,
438 'ic_dropleft_focus': LINK_SELECTED_BACKGROUND,
439 'ic_dropright_focus': LINK_SELECTED_BACKGROUND,
440 'ic_globe': LANG_HEADER_BACKGROUND,
441 'ic_search_focus': LINK_SELECTED_BACKGROUND,
442 'ic_settings_focus': LINK_SELECTED_BACKGROUND,
443 'ic_power_focus': LINK_SELECTED_BACKGROUND,
444 }
445
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800446 def __init__(self, board, formats, board_config, output):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800447 """Inits converter.
448
449 Args:
450 board: Board name.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800451 formats: A dictionary of string formats.
452 board_config: A dictionary of board configurations.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800453 output: Output directory.
454 """
455 self.board = board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800456 self.formats = formats
457 self.config = board_config
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800458 self.set_dirs(output)
459 self.set_screen()
460 self.set_replace_map()
461 self.set_locales()
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800462 self.text_max_colors = self.get_text_colors(self.config[DPI_KEY])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800463
464 def set_dirs(self, output):
465 """Sets board output directory and stage directory.
466
467 Args:
468 output: Output directory.
469 """
470 self.output_dir = os.path.join(output, self.board)
471 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
472 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
473 self.stage_dir = os.path.join(output, '.stage')
474 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
475
476 def set_screen(self):
477 """Sets screen width and height."""
478 self.screen_width, self.screen_height = self.config[SCREEN_KEY]
479
Yu-Ping Wue445e042020-11-19 15:53:42 +0800480 self.panel_stretch = fractions.Fraction(1)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800481 if self.config[PANEL_KEY]:
Yu-Ping Wue445e042020-11-19 15:53:42 +0800482 # Calculate `panel_stretch`. It's used to shrink images horizontally so
483 # that the resulting images will look proportional to the original image
484 # on the stretched display. If the display is not stretched, meaning the
485 # aspect ratio is same as the screen where images were rendered, no
486 # shrinking is performed.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800487 panel_width, panel_height = self.config[PANEL_KEY]
Yu-Ping Wue445e042020-11-19 15:53:42 +0800488 self.panel_stretch = fractions.Fraction(self.screen_width * panel_height,
489 self.screen_height * panel_width)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800490
Yu-Ping Wue445e042020-11-19 15:53:42 +0800491 if self.panel_stretch > 1:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800492 raise BuildImageError('Panel aspect ratio (%f) is smaller than screen '
493 'aspect ratio (%f). It indicates screen will be '
494 'shrunk horizontally. It is currently unsupported.'
495 % (panel_width / panel_height,
496 self.screen_width / self.screen_height))
497
498 # Set up square drawing area
499 self.canvas_px = min(self.screen_width, self.screen_height)
500
501 def set_replace_map(self):
502 """Sets a map replacing images.
503
504 For each (key, value), image 'key' will be replaced by image 'value'.
505 """
506 replace_map = self.DEFAULT_REPLACE_MAP.copy()
507
508 if os.getenv('DETACHABLE') == '1':
509 replace_map.update({
510 'nav-key_enter': 'nav-button_power',
511 'nav-key_up': 'nav-button_volume_up',
512 'nav-key_down': 'nav-button_volume_down',
513 'navigate0': 'navigate0_tablet',
514 'navigate1': 'navigate1_tablet',
515 'broken_desc': 'broken_desc_detach',
516 })
517
518 physical_presence = os.getenv('PHYSICAL_PRESENCE')
519 if physical_presence == 'recovery':
520 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_phyrec'
521 replace_map['broken_desc'] = 'broken_desc_phyrec'
522 elif physical_presence == 'power':
523 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_power'
524 elif physical_presence != 'keyboard':
525 raise BuildImageError('Invalid physical presence setting %s for board %s'
526 % (physical_presence, self.board))
527
528 if not self.config[SDCARD_KEY]:
529 replace_map.update({
530 'rec_sel_desc1': 'rec_sel_desc1_no_sd',
531 'rec_sel_desc1_no_phone': 'rec_sel_desc1_no_phone_no_sd',
532 'rec_disk_step1_desc0': 'rec_disk_step1_desc0_no_sd',
533 })
534
535 self.replace_map = replace_map
536
537 def set_locales(self):
538 """Sets a list of locales for which localized images are converted."""
539 # LOCALES environment variable can overwrite boards.yaml
540 env_locales = os.getenv('LOCALES')
541 rtl_locales = set(self.config[RTL_KEY])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800542 if env_locales:
543 locales = env_locales.split()
544 else:
545 locales = self.config[LOCALES_KEY]
546 # Check rtl_locales are contained in locales.
547 unknown_rtl_locales = rtl_locales - set(locales)
548 if unknown_rtl_locales:
549 raise BuildImageError('Unknown locales %s in %s' %
550 (list(unknown_rtl_locales), RTL_KEY))
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800551 self.locales = [LocaleInfo(code, code in rtl_locales)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800552 for code in locales]
553
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800554 @classmethod
555 def get_text_colors(cls, dpi):
556 """Derive maximum text colors from `dpi`."""
557 if dpi < 64:
558 return 2
559 elif dpi < 72:
560 return 3
561 elif dpi < 80:
562 return 4
563 elif dpi < 96:
564 return 5
565 elif dpi < 112:
566 return 6
567 else:
568 return 7
569
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800570 def _to_px(self, length, num_lines=1):
571 """Converts the relative coordinate to absolute one in pixels."""
572 return int(self.canvas_px * length / self.SCALE_BASE) * num_lines
573
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800574 def _get_png_height(self, png_file):
575 with Image.open(png_file) as image:
576 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800577
578 def get_num_lines(self, file, one_line_dir):
579 """Gets the number of lines of text in `file`."""
580 name, _ = os.path.splitext(os.path.basename(file))
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800581 png_name = name + '.png'
582 multi_line_file = os.path.join(os.path.dirname(file), png_name)
583 one_line_file = os.path.join(one_line_dir, png_name)
584 # The number of lines is determined by comparing the height of
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800585 # `multi_line_file` with `one_line_file`, where the latter is generated
586 # without the '--width' option passed to pango-view.
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800587 height = self._get_png_height(multi_line_file)
588 line_height = self._get_png_height(one_line_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800589 return int(round(height / line_height))
590
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800591 def convert_svg_to_png(self, svg_file, png_file, height, num_lines,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800592 background):
593 """Converts .svg file to .png file."""
594 background_hex = ''.join(format(x, '02x') for x in background)
595 # If the width/height of the SVG file is specified in points, the
596 # rsvg-convert command with default 90DPI will potentially cause the pixels
597 # at the right/bottom border of the output image to be transparent (or
598 # filled with the specified background color). This seems like an
599 # rsvg-convert issue regarding image scaling. Therefore, use 72DPI here
600 # to avoid the scaling.
601 command = ['rsvg-convert',
602 '--background-color', "'#%s'" % background_hex,
603 '--dpi-x', '72',
604 '--dpi-y', '72',
605 '-o', png_file]
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800606 height_px = self._to_px(height, num_lines)
Yu-Ping Wue445e042020-11-19 15:53:42 +0800607 if height_px <= 0:
608 raise BuildImageError('Height of %r <= 0 (%dpx)' %
609 (os.path.basename(svg_file), height_px))
610 command.extend(['--height', '%d' % height_px])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800611 command.append(svg_file)
612 subprocess.check_call(' '.join(command), shell=True)
613
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800614 def convert_to_bitmap(self, input_file, height, num_lines, background, output,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800615 max_colors):
616 """Converts an image file `input_file` to a BMP file `output`."""
617 image = Image.open(input_file)
618
619 # Process alpha channel and transparency.
620 if image.mode == 'RGBA':
621 target = Image.new('RGB', image.size, background)
622 image.load() # required for image.split()
623 mask = image.split()[-1]
624 target.paste(image, mask=mask)
625 elif (image.mode == 'P') and ('transparency' in image.info):
626 exit('Sorry, PNG with RGBA palette is not supported.')
627 elif image.mode != 'RGB':
628 target = image.convert('RGB')
629 else:
630 target = image
631
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800632 width_px, height_px = image.size
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800633 max_height_px = self._to_px(height, num_lines)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800634 # If the image size is larger than what will be displayed at runtime,
635 # downscale it.
636 if height_px > max_height_px:
637 height_px = max_height_px
638 width_px = height_px * image.size[0] // image.size[1]
639 # Stretch image horizontally for stretched display.
Yu-Ping Wue445e042020-11-19 15:53:42 +0800640 if self.panel_stretch != 1:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800641 width_px = int(width_px * self.panel_stretch)
642 new_size = width_px, height_px
643 if new_size != image.size:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800644 target = target.resize(new_size, Image.BICUBIC)
645
646 # Export and downsample color space.
647 target.convert('P', dither=None, colors=max_colors, palette=Image.ADAPTIVE
648 ).save(output)
649
650 with open(output, 'rb+') as f:
651 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
652 f.write(bytearray([num_lines]))
653
Yu-Ping Wued95df32020-11-04 17:08:15 +0800654 def convert(self, files, output_dir, heights, max_widths, max_colors,
655 one_line_dir=None):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800656 """Converts file(s) to bitmap format."""
657 if not files:
658 raise BuildImageError('Unable to find file(s) to convert')
659
660 for file in files:
661 name, ext = os.path.splitext(os.path.basename(file))
662 output = os.path.join(output_dir, name + self.DEFAULT_OUTPUT_EXT)
663
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800664 if name in self.replace_map:
665 name = self.replace_map[name]
666 if not name:
667 continue
668 print('Replace: %s => %s' % (file, name))
669 file = os.path.join(os.path.dirname(file), name + ext)
670
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800671 background = self.BACKGROUND_COLORS.get(name, self.DEFAULT_BACKGROUND)
672 height = heights[name]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800673 max_width = max_widths[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800674
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800675 # Determine num_lines in order to scale the image
Yu-Ping Wued95df32020-11-04 17:08:15 +0800676 if one_line_dir and max_width:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800677 num_lines = self.get_num_lines(file, one_line_dir)
678 else:
679 num_lines = 1
680
681 if ext == '.svg':
682 png_file = os.path.join(self.temp_dir, name + '.png')
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800683 self.convert_svg_to_png(file, png_file, height, num_lines, background)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800684 file = png_file
685
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800686 self.convert_to_bitmap(file, height, num_lines, background, output,
687 max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800688
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800689 def convert_sprite_images(self):
690 """Converts sprite images."""
691 names = self.formats[KEY_SPRITE_FILES]
692 styles = self.formats[KEY_STYLES]
693 # Check redundant images
694 for filename in glob.glob(os.path.join(self.ASSET_DIR, SVG_FILES)):
695 name, _ = os.path.splitext(os.path.basename(filename))
696 if name not in names:
697 raise BuildImageError('Sprite image %r not specified in %s' %
698 (filename, FORMAT_FILE))
699 # Convert images
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800700 files = []
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800701 heights = {}
702 for name, category in names.items():
703 style = get_config_with_defaults(styles, category)
704 files.append(os.path.join(self.ASSET_DIR, name + '.svg'))
705 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800706 max_widths = defaultdict(lambda: None)
707 self.convert(files, self.output_dir, heights, max_widths,
708 self.ASSET_MAX_COLORS)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800709
710 def convert_generic_strings(self):
711 """Converts generic (locale-independent) strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800712 names = self.formats[KEY_GENERIC_FILES]
713 styles = self.formats[KEY_STYLES]
714 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800715 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800716 for name, category in names.items():
717 style = get_config_with_defaults(styles, category)
718 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800719 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800720
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800721 files = glob.glob(os.path.join(self.stage_dir, PNG_FILES))
Yu-Ping Wued95df32020-11-04 17:08:15 +0800722 self.convert(files, self.output_dir, heights, max_widths,
723 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800724
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800725 def _check_text_width(self, output_dir, heights, max_widths):
726 """Check if the width of text image will exceed canvas boundary."""
727 for filename in glob.glob(os.path.join(output_dir,
728 '*' + self.DEFAULT_OUTPUT_EXT)):
729 name, _ = os.path.splitext(os.path.basename(filename))
730 max_width = max_widths[name]
731 if not max_width:
732 continue
733 max_width_px = self._to_px(max_width)
734 with open(filename, 'rb') as f:
735 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
736 num_lines = f.read(1)[0]
737 height_px = self._to_px(heights[name] * num_lines)
738 with Image.open(filename) as image:
739 width_px = height_px * image.size[0] // image.size[1]
740 if width_px > max_width_px:
741 raise BuildImageError('%s: Image width %dpx greater than max width '
742 '%dpx' % (filename, width_px, max_width_px))
743
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800744 def convert_localized_strings(self):
745 """Converts localized strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800746 names = self.formats[KEY_LOCALIZED_FILES].copy()
747 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800748 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800749 styles = self.formats[KEY_STYLES]
750 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800751 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800752 for name, category in names.items():
753 style = get_config_with_defaults(styles, category)
754 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800755 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800756
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800757 # Using stderr to report progress synchronously
758 print(' processing:', end='', file=sys.stderr, flush=True)
759 for locale_info in self.locales:
760 locale = locale_info.code
761 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
762 stage_locale_dir = os.path.join(STAGE_LOCALE_DIR, locale)
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800763 print(' ' + locale, end='', file=sys.stderr, flush=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800764 os.makedirs(ro_locale_dir)
765 self.convert(
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800766 glob.glob(os.path.join(stage_locale_dir, PNG_FILES)),
Yu-Ping Wued95df32020-11-04 17:08:15 +0800767 ro_locale_dir, heights, max_widths, self.text_max_colors,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800768 one_line_dir=os.path.join(stage_locale_dir, ONE_LINE_DIR))
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800769 self._check_text_width(ro_locale_dir, heights, max_widths)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800770 print(file=sys.stderr)
771
772 def move_language_images(self):
773 """Renames language bitmaps and move to self.output_dir.
774
775 The directory self.output_dir contains locale-independent images, and is
776 used for creating vbgfx.bin by archive_images.py.
777 """
778 for locale_info in self.locales:
779 locale = locale_info.code
780 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
781 old_file = os.path.join(ro_locale_dir, 'language.bmp')
782 new_file = os.path.join(self.output_dir, 'language_%s.bmp' % locale)
783 if os.path.exists(new_file):
784 raise BuildImageError('File already exists: %s' % new_file)
785 shutil.move(old_file, new_file)
786
787 def convert_fonts(self):
788 """Converts font images"""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800789 heights = defaultdict(lambda: self.DEFAULT_FONT_HEIGHT)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800790 max_widths = defaultdict(lambda: None)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800791 files = glob.glob(os.path.join(STAGE_FONT_DIR, SVG_FILES))
792 font_output_dir = os.path.join(self.output_dir, 'font')
793 os.makedirs(font_output_dir)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800794 self.convert(files, font_output_dir, heights, max_widths,
795 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800796
797 def copy_images_to_rw(self):
798 """Copies localized images specified in boards.yaml for RW override."""
799 if not self.config[RW_OVERRIDE_KEY]:
800 print(' No localized images are specified for RW, skipping')
801 return
802
803 for locale_info in self.locales:
804 locale = locale_info.code
805 rw_locale_dir = os.path.join(self.output_ro_dir, locale)
806 ro_locale_dir = os.path.join(self.output_rw_dir, locale)
807 os.makedirs(rw_locale_dir)
808
809 for name in self.config[RW_OVERRIDE_KEY]:
810 ro_src = os.path.join(ro_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
811 rw_dst = os.path.join(rw_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
812 shutil.copyfile(ro_src, rw_dst)
813
814 def create_locale_list(self):
815 """Creates locale list as a CSV file.
816
817 Each line in the file is of format "code,rtl", where
818 - "code": language code of the locale
819 - "rtl": "1" for right-to-left language, "0" otherwise
820 """
821 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
822 for locale_info in self.locales:
823 f.write('{},{}\n'.format(locale_info.code,
824 int(locale_info.rtl)))
825
826 def build(self):
827 """Builds all images required by a board."""
828 # Clean up output directory
829 if os.path.exists(self.output_dir):
830 shutil.rmtree(self.output_dir)
831 os.makedirs(self.output_dir)
832
833 if not os.path.exists(self.stage_dir):
834 raise BuildImageError('Missing stage folder. Run make in strings dir.')
835
836 # Clean up temp directory
837 if os.path.exists(self.temp_dir):
838 shutil.rmtree(self.temp_dir)
839 os.makedirs(self.temp_dir)
840
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800841 print('Converting sprite images...')
842 self.convert_sprite_images()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800843
844 print('Converting generic strings...')
845 self.convert_generic_strings()
846
847 print('Converting localized strings...')
848 self.convert_localized_strings()
849
850 print('Moving language images to locale-independent directory...')
851 self.move_language_images()
852
853 print('Creating locale list file...')
854 self.create_locale_list()
855
856 print('Converting fonts...')
857 self.convert_fonts()
858
859 print('Copying specified images to RW packing directory...')
860 self.copy_images_to_rw()
861
862
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800863def main():
864 """Builds bitmaps for firmware screens."""
865 parser = argparse.ArgumentParser()
866 parser.add_argument('board', help='Target board')
867 args = parser.parse_args()
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800868 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800869
870 with open(FORMAT_FILE, encoding='utf-8') as f:
871 formats = yaml.load(f)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800872 board_config = load_boards_config(BOARDS_CONFIG_FILE)[board]
873
874 # TODO(yupingso): Put everything into Converter class
875 print('Building for ' + board)
876 build_strings(formats, board_config)
877 converter = Converter(board, formats, board_config, OUTPUT_DIR)
878 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800879
880
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800881if __name__ == '__main__':
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800882 main()