blob: 28863a317e37718a1677d6b78979ce26c8823039 [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
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080010import glob
Jes Klinke1687a992020-06-16 13:47:17 -070011import json
Hung-Te Lin04addcc2015-03-23 18:43:30 +080012import multiprocessing
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080013import os
14import re
Jes Klinke1687a992020-06-16 13:47:17 -070015import shutil
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080016import signal
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080017import subprocess
18import sys
Jes Klinke1687a992020-06-16 13:47:17 -070019import tempfile
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080020from xml.etree import ElementTree
Hung-Te Lin04addcc2015-03-23 18:43:30 +080021
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080022import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080023from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080024
25SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080026
27STRINGS_GRD_FILE = 'firmware_strings.grd'
28STRINGS_JSON_FILE_TMPL = '{}.json'
29FORMAT_FILE = 'format.yaml'
30BOARDS_CONFIG_FILE = 'boards.yaml'
31
32TXT_TO_PNG_SVG = os.path.join(SCRIPT_BASE, 'text_to_png_svg')
33STRINGS_DIR = os.path.join(SCRIPT_BASE, 'strings')
34LOCALE_DIR = os.path.join(STRINGS_DIR, 'locale')
35OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
36STAGE_DIR = os.path.join(OUTPUT_DIR, '.stage')
37STAGE_LOCALE_DIR = os.path.join(STAGE_DIR, 'locale')
38STAGE_FONT_DIR = os.path.join(STAGE_DIR, 'font')
39
40ONE_LINE_DIR = 'one_line'
41SVG_FILES = '*.svg'
42PNG_FILES = '*.png'
43
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080044DIAGNOSTIC_UI = os.getenv('DIAGNOSTIC_UI') == '1'
45
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080046# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080047KEY_DEFAULT = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080048KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080049KEY_GENERIC_FILES = 'generic_files'
50KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080051KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
52KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080053KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080054KEY_BGCOLOR = 'bgcolor'
55KEY_FGCOLOR = 'fgcolor'
56KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080057KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080058KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080059
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080060# Board config YAML key names.
61SCREEN_KEY = 'screen'
62PANEL_KEY = 'panel'
63SDCARD_KEY = 'sdcard'
64BAD_USB3_KEY = 'bad_usb3'
65LOCALES_KEY = 'locales'
66RTL_KEY = 'rtl'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080067TEXT_COLORS_KEY = 'text_colors'
68RW_OVERRIDE_KEY = 'rw_override'
69
70BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080071
Jes Klinke1687a992020-06-16 13:47:17 -070072# Regular expressions used to eliminate spurious spaces and newlines in
73# translation strings.
74NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
75NEWLINE_REPLACEMENT = r'\1 \2'
76CRLF_PATTERN = re.compile(r'\r\n')
77MULTIBLANK_PATTERN = re.compile(r' *')
78
Yu-Ping Wu11027f02020-10-14 17:35:42 +080079GLYPH_FONT = 'Noto Sans Mono'
80
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080081LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080082
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080083
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080084class DataError(Exception):
85 pass
86
87
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080088class BuildImageError(Exception):
89 """The exception class for all errors generated during build image process."""
90
91
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080092def get_config_with_defaults(configs, key):
93 """Gets config of `key` from `configs`.
94
95 If `key` is not present in `configs`, the default config will be returned.
96 Similarly, if some config values are missing for `key`, the default ones will
97 be used.
98 """
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080099 config = configs[KEY_DEFAULT].copy()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800100 config.update(configs.get(key, {}))
101 return config
102
103
Yu-Ping Wued95df32020-11-04 17:08:15 +0800104def convert_text_to_png(locale, input_file, font, output_dir, height=None,
105 max_width=None, margin='0', bgcolor='#000000',
106 fgcolor='#ffffff', **options):
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800107 """Converts text files into PNG image files.
108
109 Args:
110 locale: Locale (language) to select implicit rendering options. None for
111 locale-independent strings.
112 input_file: Path of input text file.
113 font: Font spec.
Yu-Ping Wued95df32020-11-04 17:08:15 +0800114 height: Height.
115 max_width: Maximum width.
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800116 margin: CSS-style margin.
117 output_dir: Directory to generate image files.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800118 bgcolor: Background color (#rrggbb).
119 fgcolor: Foreground color (#rrggbb).
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800120 **options: Other options to be added.
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800121 """
122 name, _ = os.path.splitext(os.path.basename(input_file))
123 command = [TXT_TO_PNG_SVG, '--outdir=%s' % output_dir]
124 if locale:
125 command.append('--lan=%s' % locale)
126 if font:
127 command.append("--font='%s'" % font)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800128 font_size = os.getenv('FONT_SIZE')
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800129 if font_size:
130 command.append('--point=%r' % font_size)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800131 if max_width:
132 # Without the --width option set, the minimum height of the output SVG
133 # image is roughly 22px (for locale 'en'). With --width=WIDTH passed to
134 # pango-view, the width of the output seems to always be (WIDTH * 4 / 3),
135 # regardless of the font being used. Therefore, set the max_width in
136 # points as follows to prevent drawing from exceeding canvas boundary in
137 # depthcharge runtime.
138 max_width_pt = int(22 * max_width / height / (4 / 3))
139 command.append('--width=%d' % max_width_pt)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800140 if margin:
141 command.append('--margin="%s"' % margin)
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800142 command.append('--bgcolor="%s"' % bgcolor)
143 command.append('--color="%s"' % fgcolor)
144
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800145 for k, v in options.items():
146 command.append('--%s="%s"' % (k, v))
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800147 command.append(input_file)
148
149 return subprocess.call(' '.join(command), shell=True,
150 stdout=subprocess.PIPE) == 0
151
152
153def convert_glyphs():
154 """Converts glyphs of ascii characters."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800155 os.makedirs(STAGE_FONT_DIR, exist_ok=True)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800156 # Remove the extra whitespace at the top/bottom within the glyphs
157 margin = '-3 0 -1 0'
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800158 for c in range(ord(' '), ord('~') + 1):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800159 txt_file = os.path.join(STAGE_FONT_DIR, f'idx{c:03d}_{c:02x}.txt')
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800160 with open(txt_file, 'w', encoding='ascii') as f:
161 f.write(chr(c))
162 f.write('\n')
163 # TODO(b/163109632): Parallelize the conversion of glyphs
Yu-Ping Wued95df32020-11-04 17:08:15 +0800164 convert_text_to_png(None, txt_file, GLYPH_FONT, STAGE_FONT_DIR,
165 margin=margin)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800166
167
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800168def _load_locale_json_file(locale, json_dir):
Jes Klinke1687a992020-06-16 13:47:17 -0700169 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800170 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800171 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700172 for tag, msgdict in json.load(input_file).items():
173 msgtext = msgdict['message']
174 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
175 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
176 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
177 # Strip any trailing whitespace. A trailing newline appears to make
178 # Pango report a larger layout size than what's actually visible.
179 msgtext = msgtext.strip()
180 result[tag] = msgtext
181 return result
182
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800183
184def parse_locale_json_file(locale, json_dir):
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800185 """Parses given firmware string json file.
Mathew King89d48c62019-02-15 10:08:39 -0700186
187 Args:
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800188 locale: The name of the locale, e.g. "da" or "pt-BR".
Jes Klinke1687a992020-06-16 13:47:17 -0700189 json_dir: Directory containing json output from grit.
Mathew King89d48c62019-02-15 10:08:39 -0700190
191 Returns:
192 A dictionary for mapping of "name to content" for files to be generated.
193 """
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800194 result = _load_locale_json_file(locale, json_dir)
195 original = _load_locale_json_file('en', json_dir)
196 for tag in original:
197 if tag not in result:
198 # Use original English text, in case translation is not yet available
199 print('WARNING: locale "%s", missing entry %s' % (locale, tag))
200 result[tag] = original[tag]
Mathew King89d48c62019-02-15 10:08:39 -0700201
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800202 return result
Mathew King89d48c62019-02-15 10:08:39 -0700203
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800204
205def parse_locale_input_files(locale, json_dir):
206 """Parses all firmware string files for the given locale.
207
208 Args:
209 locale: The name of the locale, e.g. "da" or "pt-BR".
210 json_dir: Directory containing json output from grit.
211
212 Returns:
213 A dictionary for mapping of "name to content" for files to be generated.
214 """
215 result = parse_locale_json_file(locale, json_dir)
216
217 # Walk locale directory to add pre-generated texts such as language names.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800218 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Mathew King89d48c62019-02-15 10:08:39 -0700219 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800220 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700221 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700222
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800223 return result
224
225
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800226def build_text_files(inputs, files, output_dir):
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800227 """Builds text files from given input data.
228
229 Args:
230 inputs: Dictionary of contents for given file name.
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800231 files: List of files.
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800232 output_dir: Directory to generate text files.
233 """
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800234 for name in files:
235 file_name = os.path.join(output_dir, name + '.txt')
236 with open(file_name, 'w', encoding='utf-8-sig') as f:
237 f.write(inputs[name] + '\n')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800238
239
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800240def convert_localized_strings(formats):
241 """Converts localized strings."""
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800242 # Make a copy of formats to avoid modifying it
243 formats = copy.deepcopy(formats)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800244
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800245 env_locales = os.getenv('LOCALES')
246 if env_locales:
247 locales = env_locales.split()
248 else:
249 locales = formats[KEY_LOCALES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800250
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800251 files = formats[KEY_LOCALIZED_FILES]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800252 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800253 files.update(formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800254
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800255 styles = formats[KEY_STYLES]
256 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800257 default_font = fonts[KEY_DEFAULT]
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800258
Yu-Ping Wu51940352020-09-17 08:48:55 +0800259 # Sources are one .grd file with identifiers chosen by engineers and
260 # corresponding English texts, as well as a set of .xlt files (one for each
261 # language other than US english) with a mapping from hash to translation.
262 # Because the keys in the xlt files are a hash of the English source text,
263 # rather than our identifiers, such as "btn_cancel", we use the "grit"
264 # command line tool to process the .grd and .xlt files, producing a set of
265 # .json files mapping our identifier to the translated string, one for every
266 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700267
Yu-Ping Wu51940352020-09-17 08:48:55 +0800268 # Create a temporary directory to place the translation output from grit in.
269 json_dir = tempfile.mkdtemp()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800270
Yu-Ping Wu51940352020-09-17 08:48:55 +0800271 # This invokes the grit build command to generate JSON files from the XTB
272 # files containing translations. The results are placed in `json_dir` as
273 # specified in firmware_strings.grd, i.e. one JSON file per locale.
274 subprocess.check_call([
275 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800276 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800277 'build',
278 '-o', os.path.join(json_dir)
279 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700280
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800281 # Ignore SIGINT in child processes
282 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800283 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800284 signal.signal(signal.SIGINT, sigint_handler)
285
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800286 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800287 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800288 print(locale, end=' ', flush=True)
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800289 inputs = parse_locale_input_files(locale, json_dir)
290 output_dir = os.path.normpath(os.path.join(STAGE_DIR, 'locale', locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800291 if not os.path.exists(output_dir):
292 os.makedirs(output_dir)
Matt Delco4c5580d2019-03-07 14:00:28 -0800293
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800294 build_text_files(inputs, files, output_dir)
Shelley Chen2f616ac2017-05-22 13:19:40 -0700295
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800296 for name, category in files.items():
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800297 style = get_config_with_defaults(styles, category)
298 args = (
299 locale,
300 os.path.join(output_dir, '%s.txt' % name),
301 fonts.get(locale, default_font),
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800302 output_dir,
303 )
304 kwargs = {
Yu-Ping Wued95df32020-11-04 17:08:15 +0800305 'height': style[KEY_HEIGHT],
306 'max_width': style[KEY_MAX_WIDTH],
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800307 'bgcolor': style[KEY_BGCOLOR],
308 'fgcolor': style[KEY_FGCOLOR],
309 }
310 results.append(pool.apply_async(convert_text_to_png, args, kwargs))
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800311 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700312 if json_dir is not None:
313 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800314 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800315
316 try:
317 success = [r.get() for r in results]
318 except KeyboardInterrupt:
319 pool.terminate()
320 pool.join()
321 exit('Aborted by user')
322 else:
323 pool.join()
324 if not all(success):
325 exit('Failed to render some locales')
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800326
327
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800328def build_strings(formats):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800329 """Builds text strings."""
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800330 # Convert glyphs
331 print('Converting glyphs...')
332 convert_glyphs()
333
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800334 # Convert generic (locale-independent) strings
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800335 files = formats[KEY_GENERIC_FILES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800336 styles = formats[KEY_STYLES]
337 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800338 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800339
340 for input_file in glob.glob(os.path.join(STRINGS_DIR, '*.txt')):
341 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800342 category = files[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800343 style = get_config_with_defaults(styles, category)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800344 if not convert_text_to_png(None, input_file, default_font, STAGE_DIR,
345 height=style[KEY_HEIGHT],
346 max_width=style[KEY_MAX_WIDTH],
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800347 bgcolor=style[KEY_BGCOLOR],
348 fgcolor=style[KEY_FGCOLOR]):
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800349 exit('Failed to convert text %s' % input_file)
350
351 # Convert localized strings
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800352 convert_localized_strings(formats)
353
354
355def load_boards_config(filename):
356 """Loads the configuration of all boards from `filename`.
357
358 Args:
359 filename: File name of a YAML config file.
360
361 Returns:
362 A dictionary mapping each board name to its config.
363 """
364 with open(filename, 'rb') as file:
365 raw = yaml.load(file)
366
367 configs = {}
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800368 default = raw[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800369 if not default:
370 raise BuildImageError('Default configuration is not found')
371 for boards, params in raw.items():
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800372 if boards == KEY_DEFAULT:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800373 continue
374 config = copy.deepcopy(default)
375 if params:
376 config.update(params)
377 for board in boards.replace(',', ' ').split():
378 configs[board] = config
379
380 return configs
381
382
383class Converter(object):
384 """Converter from assets, texts, URLs, and fonts to bitmap images.
385
386 Attributes:
387 ASSET_DIR (str): Directory of image assets.
388 DEFAULT_OUTPUT_EXT (str): Default output file extension.
389 DEFAULT_REPLACE_MAP (dict): Default mapping of file replacement. For
390 {'a': 'b'}, "a.*" will be converted to "b.*".
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800391 SCALE_BASE (int): The base for bitmap scales, same as UI_SCALE in
392 depthcharge. For example, if `SCALE_BASE` is 1000, then height = 200 means
393 20% of the screen height. Also see the 'styles' section in format.yaml.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800394 DEFAULT_FONT_HEIGHT (tuple): Height of the font images.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800395 ASSET_MAX_COLORS (int): Maximum colors to use for converting image assets
396 to bitmaps.
397 DEFAULT_BACKGROUND (tuple): Default background color.
398 BACKGROUND_COLORS (dict): Background color of each image. Key is the image
399 name and value is a tuple of RGB values.
400 """
401
402 ASSET_DIR = 'assets'
403 DEFAULT_OUTPUT_EXT = '.bmp'
404
405 DEFAULT_REPLACE_MAP = {
406 'rec_sel_desc1_no_sd': '',
407 'rec_sel_desc1_no_phone_no_sd': '',
408 'rec_disk_step1_desc0_no_sd': '',
409 'rec_to_dev_desc1_phyrec': '',
410 'rec_to_dev_desc1_power': '',
411 'navigate0_tablet': '',
412 'navigate1_tablet': '',
413 'nav-button_power': '',
414 'nav-button_volume_up': '',
415 'nav-button_volume_down': '',
416 'broken_desc_phyrec': '',
417 'broken_desc_detach': '',
418 }
419
420 # scales
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800421 SCALE_BASE = 1000
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800422 DEFAULT_FONT_HEIGHT = 20
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800423
424 # background colors
425 DEFAULT_BACKGROUND = (0x20, 0x21, 0x24)
426 LANG_HEADER_BACKGROUND = (0x16, 0x17, 0x19)
427 LINK_SELECTED_BACKGROUND = (0x2a, 0x2f, 0x39)
428 ASSET_MAX_COLORS = 128
429
430 BACKGROUND_COLORS = {
431 'ic_dropdown': LANG_HEADER_BACKGROUND,
432 'ic_dropleft_focus': LINK_SELECTED_BACKGROUND,
433 'ic_dropright_focus': LINK_SELECTED_BACKGROUND,
434 'ic_globe': LANG_HEADER_BACKGROUND,
435 'ic_search_focus': LINK_SELECTED_BACKGROUND,
436 'ic_settings_focus': LINK_SELECTED_BACKGROUND,
437 'ic_power_focus': LINK_SELECTED_BACKGROUND,
438 }
439
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800440 def __init__(self, board, formats, board_config, output):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800441 """Inits converter.
442
443 Args:
444 board: Board name.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800445 formats: A dictionary of string formats.
446 board_config: A dictionary of board configurations.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800447 output: Output directory.
448 """
449 self.board = board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800450 self.formats = formats
451 self.config = board_config
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800452 self.set_dirs(output)
453 self.set_screen()
454 self.set_replace_map()
455 self.set_locales()
456 self.text_max_colors = self.config[TEXT_COLORS_KEY]
457
458 def set_dirs(self, output):
459 """Sets board output directory and stage directory.
460
461 Args:
462 output: Output directory.
463 """
464 self.output_dir = os.path.join(output, self.board)
465 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
466 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
467 self.stage_dir = os.path.join(output, '.stage')
468 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
469
470 def set_screen(self):
471 """Sets screen width and height."""
472 self.screen_width, self.screen_height = self.config[SCREEN_KEY]
473
474 self.stretch = (1, 1)
475 if self.config[PANEL_KEY]:
476 # Calculate 'stretch'. It's used to shrink images horizontally so that
477 # resulting images will look proportional to the original image on the
478 # stretched display. If the display is not stretched, meaning aspect
479 # ratio is same as the screen where images were rendered (1366x766),
480 # no shrinking is performed.
481 panel_width, panel_height = self.config[PANEL_KEY]
482 self.stretch = (self.screen_width * panel_height,
483 self.screen_height * panel_width)
484
485 if self.stretch[0] > self.stretch[1]:
486 raise BuildImageError('Panel aspect ratio (%f) is smaller than screen '
487 'aspect ratio (%f). It indicates screen will be '
488 'shrunk horizontally. It is currently unsupported.'
489 % (panel_width / panel_height,
490 self.screen_width / self.screen_height))
491
492 # Set up square drawing area
493 self.canvas_px = min(self.screen_width, self.screen_height)
494
495 def set_replace_map(self):
496 """Sets a map replacing images.
497
498 For each (key, value), image 'key' will be replaced by image 'value'.
499 """
500 replace_map = self.DEFAULT_REPLACE_MAP.copy()
501
502 if os.getenv('DETACHABLE') == '1':
503 replace_map.update({
504 'nav-key_enter': 'nav-button_power',
505 'nav-key_up': 'nav-button_volume_up',
506 'nav-key_down': 'nav-button_volume_down',
507 'navigate0': 'navigate0_tablet',
508 'navigate1': 'navigate1_tablet',
509 'broken_desc': 'broken_desc_detach',
510 })
511
512 physical_presence = os.getenv('PHYSICAL_PRESENCE')
513 if physical_presence == 'recovery':
514 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_phyrec'
515 replace_map['broken_desc'] = 'broken_desc_phyrec'
516 elif physical_presence == 'power':
517 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_power'
518 elif physical_presence != 'keyboard':
519 raise BuildImageError('Invalid physical presence setting %s for board %s'
520 % (physical_presence, self.board))
521
522 if not self.config[SDCARD_KEY]:
523 replace_map.update({
524 'rec_sel_desc1': 'rec_sel_desc1_no_sd',
525 'rec_sel_desc1_no_phone': 'rec_sel_desc1_no_phone_no_sd',
526 'rec_disk_step1_desc0': 'rec_disk_step1_desc0_no_sd',
527 })
528
529 self.replace_map = replace_map
530
531 def set_locales(self):
532 """Sets a list of locales for which localized images are converted."""
533 # LOCALES environment variable can overwrite boards.yaml
534 env_locales = os.getenv('LOCALES')
535 rtl_locales = set(self.config[RTL_KEY])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800536 if env_locales:
537 locales = env_locales.split()
538 else:
539 locales = self.config[LOCALES_KEY]
540 # Check rtl_locales are contained in locales.
541 unknown_rtl_locales = rtl_locales - set(locales)
542 if unknown_rtl_locales:
543 raise BuildImageError('Unknown locales %s in %s' %
544 (list(unknown_rtl_locales), RTL_KEY))
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800545 self.locales = [LocaleInfo(code, code in rtl_locales)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800546 for code in locales]
547
548 def calculate_dimension(self, original, scale, num_lines):
549 """Calculates scaled width and height.
550
551 This imitates the function of Depthcharge with the same name.
552
553 Args:
554 original: (width, height) of the original image.
555 scale: (x, y) scale parameter relative to the canvas size using
556 SCALE_BASE as a base.
557 num_lines: multiplication factor for the y-dimension.
558
559 Returns:
560 (width, height) of the scaled image.
561 """
562 dim_width, dim_height = (0, 0)
563 scale_x, scale_y = scale
564 org_width, org_height = original
565
566 if scale_x == 0 and scale_y == 0:
567 raise BuildImageError('Invalid scale parameter: %s' % (scale))
568 if scale_x > 0:
569 dim_width = int(self.canvas_px * scale_x / self.SCALE_BASE)
570 if scale_y > 0:
571 dim_height = int(self.canvas_px * scale_y / self.SCALE_BASE) * num_lines
572 if scale_x == 0:
573 dim_width = org_width * dim_height // org_height
574 if scale_y == 0:
575 dim_height = org_height * dim_width // org_width
576
577 dim_width = int(dim_width * self.stretch[0] / self.stretch[1])
578
579 return dim_width, dim_height
580
581 def _get_svg_height(self, svg_file):
582 tree = ElementTree.parse(svg_file)
583 height = tree.getroot().attrib['height']
584 m = re.match('([0-9]+)pt', height)
585 if not m:
586 raise BuildImageError('Cannot get height from %s' % svg_file)
587 return int(m.group(1))
588
589 def get_num_lines(self, file, one_line_dir):
590 """Gets the number of lines of text in `file`."""
591 name, _ = os.path.splitext(os.path.basename(file))
592 svg_name = name + '.svg'
593 multi_line_file = os.path.join(os.path.dirname(file), svg_name)
594 one_line_file = os.path.join(one_line_dir, svg_name)
595 # The number of lines id determined by comparing the height of
596 # `multi_line_file` with `one_line_file`, where the latter is generated
597 # without the '--width' option passed to pango-view.
598 height = self._get_svg_height(multi_line_file)
599 line_height = self._get_svg_height(one_line_file)
600 return int(round(height / line_height))
601
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800602 def convert_svg_to_png(self, svg_file, png_file, height, num_lines,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800603 background):
604 """Converts .svg file to .png file."""
605 background_hex = ''.join(format(x, '02x') for x in background)
606 # If the width/height of the SVG file is specified in points, the
607 # rsvg-convert command with default 90DPI will potentially cause the pixels
608 # at the right/bottom border of the output image to be transparent (or
609 # filled with the specified background color). This seems like an
610 # rsvg-convert issue regarding image scaling. Therefore, use 72DPI here
611 # to avoid the scaling.
612 command = ['rsvg-convert',
613 '--background-color', "'#%s'" % background_hex,
614 '--dpi-x', '72',
615 '--dpi-y', '72',
616 '-o', png_file]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800617 if height:
618 height_px = int(self.canvas_px * height / self.SCALE_BASE) * num_lines
619 command.extend(['--height', '%d' % height_px])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800620 command.append(svg_file)
621 subprocess.check_call(' '.join(command), shell=True)
622
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800623 def convert_to_bitmap(self, input_file, height, num_lines, background, output,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800624 max_colors):
625 """Converts an image file `input_file` to a BMP file `output`."""
626 image = Image.open(input_file)
627
628 # Process alpha channel and transparency.
629 if image.mode == 'RGBA':
630 target = Image.new('RGB', image.size, background)
631 image.load() # required for image.split()
632 mask = image.split()[-1]
633 target.paste(image, mask=mask)
634 elif (image.mode == 'P') and ('transparency' in image.info):
635 exit('Sorry, PNG with RGBA palette is not supported.')
636 elif image.mode != 'RGB':
637 target = image.convert('RGB')
638 else:
639 target = image
640
641 # Process scaling
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800642 if height:
643 new_size = self.calculate_dimension(image.size, (0, height), num_lines)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800644 if new_size[0] == 0 or new_size[1] == 0:
645 print('Scaling', input_file)
646 print('Warning: width or height is 0 after resizing: '
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800647 'height=%d size=%s stretch=%s new_size=%s' %
648 (height, image.size, self.stretch, new_size))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800649 return
650 target = target.resize(new_size, Image.BICUBIC)
651
652 # Export and downsample color space.
653 target.convert('P', dither=None, colors=max_colors, palette=Image.ADAPTIVE
654 ).save(output)
655
656 with open(output, 'rb+') as f:
657 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
658 f.write(bytearray([num_lines]))
659
Yu-Ping Wued95df32020-11-04 17:08:15 +0800660 def convert(self, files, output_dir, heights, max_widths, max_colors,
661 one_line_dir=None):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800662 """Converts file(s) to bitmap format."""
663 if not files:
664 raise BuildImageError('Unable to find file(s) to convert')
665
666 for file in files:
667 name, ext = os.path.splitext(os.path.basename(file))
668 output = os.path.join(output_dir, name + self.DEFAULT_OUTPUT_EXT)
669
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800670 if name in self.replace_map:
671 name = self.replace_map[name]
672 if not name:
673 continue
674 print('Replace: %s => %s' % (file, name))
675 file = os.path.join(os.path.dirname(file), name + ext)
676
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800677 background = self.BACKGROUND_COLORS.get(name, self.DEFAULT_BACKGROUND)
678 height = heights[name]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800679 max_width = max_widths[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800680
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800681 # Determine num_lines in order to scale the image
Yu-Ping Wued95df32020-11-04 17:08:15 +0800682 if one_line_dir and max_width:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800683 num_lines = self.get_num_lines(file, one_line_dir)
684 else:
685 num_lines = 1
686
687 if ext == '.svg':
688 png_file = os.path.join(self.temp_dir, name + '.png')
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800689 self.convert_svg_to_png(file, png_file, height, num_lines, background)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800690 file = png_file
691
692 self.convert_to_bitmap(
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800693 file, height, num_lines, background, output, max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800694
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800695 def convert_sprite_images(self):
696 """Converts sprite images."""
697 names = self.formats[KEY_SPRITE_FILES]
698 styles = self.formats[KEY_STYLES]
699 # Check redundant images
700 for filename in glob.glob(os.path.join(self.ASSET_DIR, SVG_FILES)):
701 name, _ = os.path.splitext(os.path.basename(filename))
702 if name not in names:
703 raise BuildImageError('Sprite image %r not specified in %s' %
704 (filename, FORMAT_FILE))
705 # Convert images
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800706 files = []
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800707 heights = {}
708 for name, category in names.items():
709 style = get_config_with_defaults(styles, category)
710 files.append(os.path.join(self.ASSET_DIR, name + '.svg'))
711 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800712 max_widths = defaultdict(lambda: None)
713 self.convert(files, self.output_dir, heights, max_widths,
714 self.ASSET_MAX_COLORS)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800715
716 def convert_generic_strings(self):
717 """Converts generic (locale-independent) strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800718 names = self.formats[KEY_GENERIC_FILES]
719 styles = self.formats[KEY_STYLES]
720 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800721 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800722 for name, category in names.items():
723 style = get_config_with_defaults(styles, category)
724 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800725 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800726
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800727 files = glob.glob(os.path.join(self.stage_dir, SVG_FILES))
Yu-Ping Wued95df32020-11-04 17:08:15 +0800728 self.convert(files, self.output_dir, heights, max_widths,
729 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800730
731 def convert_localized_strings(self):
732 """Converts localized strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800733 names = self.formats[KEY_LOCALIZED_FILES].copy()
734 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800735 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800736 styles = self.formats[KEY_STYLES]
737 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800738 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800739 for name, category in names.items():
740 style = get_config_with_defaults(styles, category)
741 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800742 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800743
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800744 # Using stderr to report progress synchronously
745 print(' processing:', end='', file=sys.stderr, flush=True)
746 for locale_info in self.locales:
747 locale = locale_info.code
748 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
749 stage_locale_dir = os.path.join(STAGE_LOCALE_DIR, locale)
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800750 print(' ' + locale, end='', file=sys.stderr, flush=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800751 os.makedirs(ro_locale_dir)
752 self.convert(
753 glob.glob(os.path.join(stage_locale_dir, SVG_FILES)),
Yu-Ping Wued95df32020-11-04 17:08:15 +0800754 ro_locale_dir, heights, max_widths, self.text_max_colors,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800755 one_line_dir=os.path.join(stage_locale_dir, ONE_LINE_DIR))
756 print(file=sys.stderr)
757
758 def move_language_images(self):
759 """Renames language bitmaps and move to self.output_dir.
760
761 The directory self.output_dir contains locale-independent images, and is
762 used for creating vbgfx.bin by archive_images.py.
763 """
764 for locale_info in self.locales:
765 locale = locale_info.code
766 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
767 old_file = os.path.join(ro_locale_dir, 'language.bmp')
768 new_file = os.path.join(self.output_dir, 'language_%s.bmp' % locale)
769 if os.path.exists(new_file):
770 raise BuildImageError('File already exists: %s' % new_file)
771 shutil.move(old_file, new_file)
772
773 def convert_fonts(self):
774 """Converts font images"""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800775 heights = defaultdict(lambda: self.DEFAULT_FONT_HEIGHT)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800776 max_widths = defaultdict(lambda: None)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800777 files = glob.glob(os.path.join(STAGE_FONT_DIR, SVG_FILES))
778 font_output_dir = os.path.join(self.output_dir, 'font')
779 os.makedirs(font_output_dir)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800780 self.convert(files, font_output_dir, heights, max_widths,
781 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800782
783 def copy_images_to_rw(self):
784 """Copies localized images specified in boards.yaml for RW override."""
785 if not self.config[RW_OVERRIDE_KEY]:
786 print(' No localized images are specified for RW, skipping')
787 return
788
789 for locale_info in self.locales:
790 locale = locale_info.code
791 rw_locale_dir = os.path.join(self.output_ro_dir, locale)
792 ro_locale_dir = os.path.join(self.output_rw_dir, locale)
793 os.makedirs(rw_locale_dir)
794
795 for name in self.config[RW_OVERRIDE_KEY]:
796 ro_src = os.path.join(ro_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
797 rw_dst = os.path.join(rw_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
798 shutil.copyfile(ro_src, rw_dst)
799
800 def create_locale_list(self):
801 """Creates locale list as a CSV file.
802
803 Each line in the file is of format "code,rtl", where
804 - "code": language code of the locale
805 - "rtl": "1" for right-to-left language, "0" otherwise
806 """
807 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
808 for locale_info in self.locales:
809 f.write('{},{}\n'.format(locale_info.code,
810 int(locale_info.rtl)))
811
812 def build(self):
813 """Builds all images required by a board."""
814 # Clean up output directory
815 if os.path.exists(self.output_dir):
816 shutil.rmtree(self.output_dir)
817 os.makedirs(self.output_dir)
818
819 if not os.path.exists(self.stage_dir):
820 raise BuildImageError('Missing stage folder. Run make in strings dir.')
821
822 # Clean up temp directory
823 if os.path.exists(self.temp_dir):
824 shutil.rmtree(self.temp_dir)
825 os.makedirs(self.temp_dir)
826
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800827 print('Converting sprite images...')
828 self.convert_sprite_images()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800829
830 print('Converting generic strings...')
831 self.convert_generic_strings()
832
833 print('Converting localized strings...')
834 self.convert_localized_strings()
835
836 print('Moving language images to locale-independent directory...')
837 self.move_language_images()
838
839 print('Creating locale list file...')
840 self.create_locale_list()
841
842 print('Converting fonts...')
843 self.convert_fonts()
844
845 print('Copying specified images to RW packing directory...')
846 self.copy_images_to_rw()
847
848
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800849def build_images(board, formats):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800850 """Builds images for `board`."""
851 configs = load_boards_config(BOARDS_CONFIG_FILE)
852 print('Building for ' + board)
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800853 converter = Converter(board, formats, configs[board], OUTPUT_DIR)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800854 converter.build()
855
856
857def main():
858 """Builds bitmaps for firmware screens."""
859 parser = argparse.ArgumentParser()
860 parser.add_argument('board', help='Target board')
861 args = parser.parse_args()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800862
863 with open(FORMAT_FILE, encoding='utf-8') as f:
864 formats = yaml.load(f)
865 build_strings(formats)
866 build_images(args.board, formats)
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800867
868
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800869if __name__ == '__main__':
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800870 main()