blob: 28265455ff4a1eca8e3deaf05405fcfcf5780099 [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
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080033STRINGS_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'
Yu-Ping Wue66a7b02020-11-19 15:18:08 +080065DPI_KEY = 'dpi'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080066LOCALES_KEY = 'locales'
67RTL_KEY = 'rtl'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080068RW_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 Wu3d07a062021-01-26 18:10:32 +080079# The base for bitmap scales, same as UI_SCALE in depthcharge. For example, if
80# `SCALE_BASE` is 1000, then height = 200 means 20% of the screen height. Also
81# see the 'styles' section in format.yaml.
82SCALE_BASE = 1000
83DEFAULT_GLYPH_HEIGHT = 20
84
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +080085GLYPH_FONT = 'Cousine'
Yu-Ping Wu11027f02020-10-14 17:35:42 +080086
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080087LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080088
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080089
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080090class DataError(Exception):
91 pass
92
93
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080094class BuildImageError(Exception):
95 """The exception class for all errors generated during build image process."""
96
97
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080098def get_config_with_defaults(configs, key):
99 """Gets config of `key` from `configs`.
100
101 If `key` is not present in `configs`, the default config will be returned.
102 Similarly, if some config values are missing for `key`, the default ones will
103 be used.
104 """
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800105 config = configs[KEY_DEFAULT].copy()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800106 config.update(configs.get(key, {}))
107 return config
108
109
Yu-Ping Wu97046932021-01-25 17:38:56 +0800110def run_pango_view(input_file, output_file, locale, font, height, max_width,
111 dpi, bgcolor, fgcolor, hinting='full'):
112 """Run pango-view."""
113 command = ['pango-view', '-q']
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800114 if locale:
Yu-Ping Wu97046932021-01-25 17:38:56 +0800115 command += ['--language', locale]
116
117 # Font size should be proportional to the height. Here we use 2 as the
118 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
119 # will be roughly equivalent to setting the screen resolution to 1366x768.
120 font_size = height / 2
121 font_spec = '%s %r' % (font, font_size)
122 command += ['--font', font_spec]
123
Yu-Ping Wued95df32020-11-04 17:08:15 +0800124 if max_width:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800125 # When converting text to PNG by pango-view, the ratio of image height to
126 # the font size is usually no more than 1.1875 (with Roboto). Therefore,
127 # set the `max_width_pt` as follows to prevent UI drawing from exceeding
128 # the canvas boundary in depthcharge runtime. The divisor 2 is the same in
129 # the calculation of `font_size` above.
130 max_width_pt = int(max_width / 2 * 1.1875)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800131 command.append('--width=%d' % max_width_pt)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800132 if dpi:
133 command.append('--dpi=%d' % dpi)
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800134 command.append('--margin=0')
Yu-Ping Wu97046932021-01-25 17:38:56 +0800135 command += ['--background', bgcolor]
136 command += ['--foreground', fgcolor]
137 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800138
Yu-Ping Wu97046932021-01-25 17:38:56 +0800139 command += ['--output', output_file]
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800140 command.append(input_file)
141
Yu-Ping Wu97046932021-01-25 17:38:56 +0800142 subprocess.check_call(command, stdout=subprocess.PIPE)
143
144
145def convert_text_to_image(locale, input_file, font, output_dir, height=None,
146 max_width=None, dpi=None, bgcolor='#000000',
147 fgcolor='#ffffff', use_svg=False):
148 """Converts text file `input_file` into image file(s).
149
150 Because pango-view does not support assigning output format options for
151 bitmap, we must create images in SVG/PNG format and then post-process them
152 (e.g. convert into BMP by ImageMagick).
153
154 Args:
155 locale: Locale (language) to select implicit rendering options. None for
156 locale-independent strings.
157 input_file: Path of input text file.
158 font: Font name.
159 height: Image height relative to the screen resolution.
160 max_width: Maximum image width relative to the screen resolution.
161 output_dir: Directory to generate image files.
162 bgcolor: Background color (#rrggbb).
163 fgcolor: Foreground color (#rrggbb).
164 use_svg: If set to True, generate SVG file. Otherwise, generate PNG file.
165 """
166 os.makedirs(os.path.join(output_dir, ONE_LINE_DIR), exist_ok=True)
167 name, _ = os.path.splitext(os.path.basename(input_file))
168 svg_file = os.path.join(output_dir, name + '.svg')
169 png_file = os.path.join(output_dir, name + '.png')
170 png_file_one_line = os.path.join(output_dir, ONE_LINE_DIR, name + '.png')
171
172 if use_svg:
173 run_pango_view(input_file, svg_file, locale, font, height, 0, dpi,
174 bgcolor, fgcolor, hinting='none')
175 else:
176 run_pango_view(input_file, png_file, locale, font, height, max_width, dpi,
177 bgcolor, fgcolor)
178 if locale:
179 run_pango_view(input_file, png_file_one_line, locale, font, height, 0,
180 dpi, bgcolor, fgcolor)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800181
182
183def convert_glyphs():
184 """Converts glyphs of ascii characters."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800185 os.makedirs(STAGE_FONT_DIR, exist_ok=True)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800186 # Remove the extra whitespace at the top/bottom within the glyphs
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800187 for c in range(ord(' '), ord('~') + 1):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800188 txt_file = os.path.join(STAGE_FONT_DIR, f'idx{c:03d}_{c:02x}.txt')
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800189 with open(txt_file, 'w', encoding='ascii') as f:
190 f.write(chr(c))
191 f.write('\n')
192 # TODO(b/163109632): Parallelize the conversion of glyphs
Yu-Ping Wu97046932021-01-25 17:38:56 +0800193 convert_text_to_image(None, txt_file, GLYPH_FONT, STAGE_FONT_DIR,
194 height=DEFAULT_GLYPH_HEIGHT, use_svg=True)
195
196
197def check_fonts(fonts):
198 """Check if all fonts are available."""
199 for locale, font in fonts.items():
200 if subprocess.run(['fc-list', '-q', font]).returncode != 0:
201 raise BuildImageError('Font %r not found for locale %r'
202 % (font, locale))
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800203
204
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800205def parse_locale_json_file(locale, json_dir):
206 """Parses given firmware string json file.
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 """
Jes Klinke1687a992020-06-16 13:47:17 -0700215 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800216 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800217 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700218 for tag, msgdict in json.load(input_file).items():
219 msgtext = msgdict['message']
220 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
221 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
222 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
223 # Strip any trailing whitespace. A trailing newline appears to make
224 # Pango report a larger layout size than what's actually visible.
225 msgtext = msgtext.strip()
226 result[tag] = msgtext
227 return result
228
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800229
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800230def parse_locale_input_files(locale, json_dir):
231 """Parses all firmware string files for the given locale.
232
233 Args:
234 locale: The name of the locale, e.g. "da" or "pt-BR".
235 json_dir: Directory containing json output from grit.
236
237 Returns:
238 A dictionary for mapping of "name to content" for files to be generated.
239 """
240 result = parse_locale_json_file(locale, json_dir)
241
242 # Walk locale directory to add pre-generated texts such as language names.
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800243 for input_file in glob.glob(os.path.join(LOCALE_DIR, locale, "*.txt")):
Mathew King89d48c62019-02-15 10:08:39 -0700244 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800245 with open(input_file, 'r', encoding='utf-8-sig') as f:
Mathew King89d48c62019-02-15 10:08:39 -0700246 result[name] = f.read().strip()
Shelley Chen2f616ac2017-05-22 13:19:40 -0700247
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800248 return result
249
250
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800251def convert_localized_strings(formats, dpi):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800252 """Converts localized strings."""
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800253 # Make a copy of formats to avoid modifying it
254 formats = copy.deepcopy(formats)
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800255
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800256 env_locales = os.getenv('LOCALES')
257 if env_locales:
258 locales = env_locales.split()
259 else:
260 locales = formats[KEY_LOCALES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800261
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800262 files = formats[KEY_LOCALIZED_FILES]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800263 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800264 files.update(formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800265
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800266 styles = formats[KEY_STYLES]
267 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800268 default_font = fonts[KEY_DEFAULT]
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800269
Yu-Ping Wu51940352020-09-17 08:48:55 +0800270 # Sources are one .grd file with identifiers chosen by engineers and
271 # corresponding English texts, as well as a set of .xlt files (one for each
272 # language other than US english) with a mapping from hash to translation.
273 # Because the keys in the xlt files are a hash of the English source text,
274 # rather than our identifiers, such as "btn_cancel", we use the "grit"
275 # command line tool to process the .grd and .xlt files, producing a set of
276 # .json files mapping our identifier to the translated string, one for every
277 # language including US English.
Jes Klinke1687a992020-06-16 13:47:17 -0700278
Yu-Ping Wu51940352020-09-17 08:48:55 +0800279 # Create a temporary directory to place the translation output from grit in.
280 json_dir = tempfile.mkdtemp()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800281
Yu-Ping Wu51940352020-09-17 08:48:55 +0800282 # This invokes the grit build command to generate JSON files from the XTB
283 # files containing translations. The results are placed in `json_dir` as
284 # specified in firmware_strings.grd, i.e. one JSON file per locale.
285 subprocess.check_call([
286 'grit',
Yu-Ping Wu8f633b82020-09-22 14:27:57 +0800287 '-i', os.path.join(LOCALE_DIR, STRINGS_GRD_FILE),
Yu-Ping Wu51940352020-09-17 08:48:55 +0800288 'build',
289 '-o', os.path.join(json_dir)
290 ])
Jes Klinke1687a992020-06-16 13:47:17 -0700291
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800292 # Ignore SIGINT in child processes
293 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800294 pool = multiprocessing.Pool(multiprocessing.cpu_count())
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800295 signal.signal(signal.SIGINT, sigint_handler)
296
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800297 results = []
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800298 for locale in locales:
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800299 print(locale, end=' ', flush=True)
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800300 inputs = parse_locale_input_files(locale, json_dir)
301 output_dir = os.path.normpath(os.path.join(STAGE_DIR, 'locale', locale))
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800302 if not os.path.exists(output_dir):
303 os.makedirs(output_dir)
Matt Delco4c5580d2019-03-07 14:00:28 -0800304
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800305 for name, category in files.items():
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800306 # Ignore missing translation
307 if locale != 'en' and name not in inputs:
308 continue
309
310 # Write to text file
311 text_file = os.path.join(output_dir, name + '.txt')
312 with open(text_file, 'w', encoding='utf-8-sig') as f:
313 f.write(inputs[name] + '\n')
314
315 # Convert to PNG file
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800316 style = get_config_with_defaults(styles, category)
317 args = (
318 locale,
319 os.path.join(output_dir, '%s.txt' % name),
320 fonts.get(locale, default_font),
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800321 output_dir,
322 )
323 kwargs = {
Yu-Ping Wued95df32020-11-04 17:08:15 +0800324 'height': style[KEY_HEIGHT],
325 'max_width': style[KEY_MAX_WIDTH],
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800326 'dpi': dpi,
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800327 'bgcolor': style[KEY_BGCOLOR],
328 'fgcolor': style[KEY_FGCOLOR],
329 }
Yu-Ping Wu97046932021-01-25 17:38:56 +0800330 results.append(pool.apply_async(convert_text_to_image, args, kwargs))
Hung-Te Lin04addcc2015-03-23 18:43:30 +0800331 pool.close()
Jes Klinke1687a992020-06-16 13:47:17 -0700332 if json_dir is not None:
333 shutil.rmtree(json_dir)
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800334 print()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800335
336 try:
Yu-Ping Wu97046932021-01-25 17:38:56 +0800337 for r in results:
338 r.get()
Yu-Ping Wuc90a22f2020-04-24 11:17:15 +0800339 except KeyboardInterrupt:
340 pool.terminate()
341 pool.join()
342 exit('Aborted by user')
343 else:
344 pool.join()
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800345
346
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800347def build_strings(formats, board_config):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800348 """Builds text strings."""
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800349 dpi = board_config[DPI_KEY]
350
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800351 # Convert glyphs
352 print('Converting glyphs...')
353 convert_glyphs()
354
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800355 # Convert generic (locale-independent) strings
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800356 files = formats[KEY_GENERIC_FILES]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800357 styles = formats[KEY_STYLES]
358 fonts = formats[KEY_FONTS]
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800359 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800360
Yu-Ping Wu97046932021-01-25 17:38:56 +0800361 check_fonts(fonts)
362
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800363 for input_file in glob.glob(os.path.join(STRINGS_DIR, '*.txt')):
364 name, _ = os.path.splitext(os.path.basename(input_file))
Yu-Ping Wu338f0832020-10-23 16:14:40 +0800365 category = files[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800366 style = get_config_with_defaults(styles, category)
Yu-Ping Wu97046932021-01-25 17:38:56 +0800367 convert_text_to_image(None, input_file, default_font, STAGE_DIR,
368 height=style[KEY_HEIGHT],
369 max_width=style[KEY_MAX_WIDTH],
370 dpi=dpi,
371 bgcolor=style[KEY_BGCOLOR],
372 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800373
374 # Convert localized strings
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800375 convert_localized_strings(formats, dpi)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800376
377
378def load_boards_config(filename):
379 """Loads the configuration of all boards from `filename`.
380
381 Args:
382 filename: File name of a YAML config file.
383
384 Returns:
385 A dictionary mapping each board name to its config.
386 """
387 with open(filename, 'rb') as file:
388 raw = yaml.load(file)
389
390 configs = {}
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800391 default = raw[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800392 if not default:
393 raise BuildImageError('Default configuration is not found')
394 for boards, params in raw.items():
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800395 if boards == KEY_DEFAULT:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800396 continue
397 config = copy.deepcopy(default)
398 if params:
399 config.update(params)
400 for board in boards.replace(',', ' ').split():
401 configs[board] = config
402
403 return configs
404
405
406class Converter(object):
407 """Converter from assets, texts, URLs, and fonts to bitmap images.
408
409 Attributes:
410 ASSET_DIR (str): Directory of image assets.
411 DEFAULT_OUTPUT_EXT (str): Default output file extension.
412 DEFAULT_REPLACE_MAP (dict): Default mapping of file replacement. For
413 {'a': 'b'}, "a.*" will be converted to "b.*".
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800414 ASSET_MAX_COLORS (int): Maximum colors to use for converting image assets
415 to bitmaps.
416 DEFAULT_BACKGROUND (tuple): Default background color.
417 BACKGROUND_COLORS (dict): Background color of each image. Key is the image
418 name and value is a tuple of RGB values.
419 """
420
421 ASSET_DIR = 'assets'
422 DEFAULT_OUTPUT_EXT = '.bmp'
423
424 DEFAULT_REPLACE_MAP = {
425 'rec_sel_desc1_no_sd': '',
426 'rec_sel_desc1_no_phone_no_sd': '',
427 'rec_disk_step1_desc0_no_sd': '',
428 'rec_to_dev_desc1_phyrec': '',
429 'rec_to_dev_desc1_power': '',
430 'navigate0_tablet': '',
431 'navigate1_tablet': '',
432 'nav-button_power': '',
433 'nav-button_volume_up': '',
434 'nav-button_volume_down': '',
435 'broken_desc_phyrec': '',
436 'broken_desc_detach': '',
437 }
438
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800439 # background colors
440 DEFAULT_BACKGROUND = (0x20, 0x21, 0x24)
441 LANG_HEADER_BACKGROUND = (0x16, 0x17, 0x19)
442 LINK_SELECTED_BACKGROUND = (0x2a, 0x2f, 0x39)
443 ASSET_MAX_COLORS = 128
444
445 BACKGROUND_COLORS = {
446 'ic_dropdown': LANG_HEADER_BACKGROUND,
447 'ic_dropleft_focus': LINK_SELECTED_BACKGROUND,
448 'ic_dropright_focus': LINK_SELECTED_BACKGROUND,
449 'ic_globe': LANG_HEADER_BACKGROUND,
450 'ic_search_focus': LINK_SELECTED_BACKGROUND,
451 'ic_settings_focus': LINK_SELECTED_BACKGROUND,
452 'ic_power_focus': LINK_SELECTED_BACKGROUND,
453 }
454
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800455 def __init__(self, board, formats, board_config, output):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800456 """Inits converter.
457
458 Args:
459 board: Board name.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800460 formats: A dictionary of string formats.
461 board_config: A dictionary of board configurations.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800462 output: Output directory.
463 """
464 self.board = board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800465 self.formats = formats
466 self.config = board_config
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800467 self.set_dirs(output)
468 self.set_screen()
469 self.set_replace_map()
470 self.set_locales()
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800471 self.text_max_colors = self.get_text_colors(self.config[DPI_KEY])
Yu-Ping Wu354a7002021-01-07 16:07:02 +0800472 self.dpi_warning_printed = False
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800473
474 def set_dirs(self, output):
475 """Sets board output directory and stage directory.
476
477 Args:
478 output: Output directory.
479 """
480 self.output_dir = os.path.join(output, self.board)
481 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
482 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
483 self.stage_dir = os.path.join(output, '.stage')
484 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
485
486 def set_screen(self):
487 """Sets screen width and height."""
488 self.screen_width, self.screen_height = self.config[SCREEN_KEY]
489
Yu-Ping Wue445e042020-11-19 15:53:42 +0800490 self.panel_stretch = fractions.Fraction(1)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800491 if self.config[PANEL_KEY]:
Yu-Ping Wue445e042020-11-19 15:53:42 +0800492 # Calculate `panel_stretch`. It's used to shrink images horizontally so
493 # that the resulting images will look proportional to the original image
494 # on the stretched display. If the display is not stretched, meaning the
495 # aspect ratio is same as the screen where images were rendered, no
496 # shrinking is performed.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800497 panel_width, panel_height = self.config[PANEL_KEY]
Yu-Ping Wue445e042020-11-19 15:53:42 +0800498 self.panel_stretch = fractions.Fraction(self.screen_width * panel_height,
499 self.screen_height * panel_width)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800500
Yu-Ping Wue445e042020-11-19 15:53:42 +0800501 if self.panel_stretch > 1:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800502 raise BuildImageError('Panel aspect ratio (%f) is smaller than screen '
503 'aspect ratio (%f). It indicates screen will be '
504 'shrunk horizontally. It is currently unsupported.'
505 % (panel_width / panel_height,
506 self.screen_width / self.screen_height))
507
508 # Set up square drawing area
509 self.canvas_px = min(self.screen_width, self.screen_height)
510
511 def set_replace_map(self):
512 """Sets a map replacing images.
513
514 For each (key, value), image 'key' will be replaced by image 'value'.
515 """
516 replace_map = self.DEFAULT_REPLACE_MAP.copy()
517
518 if os.getenv('DETACHABLE') == '1':
519 replace_map.update({
520 'nav-key_enter': 'nav-button_power',
521 'nav-key_up': 'nav-button_volume_up',
522 'nav-key_down': 'nav-button_volume_down',
523 'navigate0': 'navigate0_tablet',
524 'navigate1': 'navigate1_tablet',
525 'broken_desc': 'broken_desc_detach',
526 })
527
528 physical_presence = os.getenv('PHYSICAL_PRESENCE')
529 if physical_presence == 'recovery':
530 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_phyrec'
531 replace_map['broken_desc'] = 'broken_desc_phyrec'
532 elif physical_presence == 'power':
533 replace_map['rec_to_dev_desc1'] = 'rec_to_dev_desc1_power'
534 elif physical_presence != 'keyboard':
535 raise BuildImageError('Invalid physical presence setting %s for board %s'
536 % (physical_presence, self.board))
537
538 if not self.config[SDCARD_KEY]:
539 replace_map.update({
540 'rec_sel_desc1': 'rec_sel_desc1_no_sd',
541 'rec_sel_desc1_no_phone': 'rec_sel_desc1_no_phone_no_sd',
542 'rec_disk_step1_desc0': 'rec_disk_step1_desc0_no_sd',
543 })
544
545 self.replace_map = replace_map
546
547 def set_locales(self):
548 """Sets a list of locales for which localized images are converted."""
549 # LOCALES environment variable can overwrite boards.yaml
550 env_locales = os.getenv('LOCALES')
551 rtl_locales = set(self.config[RTL_KEY])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800552 if env_locales:
553 locales = env_locales.split()
554 else:
555 locales = self.config[LOCALES_KEY]
556 # Check rtl_locales are contained in locales.
557 unknown_rtl_locales = rtl_locales - set(locales)
558 if unknown_rtl_locales:
559 raise BuildImageError('Unknown locales %s in %s' %
560 (list(unknown_rtl_locales), RTL_KEY))
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800561 self.locales = [LocaleInfo(code, code in rtl_locales)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800562 for code in locales]
563
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800564 @classmethod
565 def get_text_colors(cls, dpi):
566 """Derive maximum text colors from `dpi`."""
567 if dpi < 64:
568 return 2
569 elif dpi < 72:
570 return 3
571 elif dpi < 80:
572 return 4
573 elif dpi < 96:
574 return 5
575 elif dpi < 112:
576 return 6
577 else:
578 return 7
579
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800580 def _to_px(self, length, num_lines=1):
581 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu3d07a062021-01-26 18:10:32 +0800582 return int(self.canvas_px * length / SCALE_BASE) * num_lines
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800583
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800584 def _get_png_height(self, png_file):
585 with Image.open(png_file) as image:
586 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800587
588 def get_num_lines(self, file, one_line_dir):
589 """Gets the number of lines of text in `file`."""
590 name, _ = os.path.splitext(os.path.basename(file))
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800591 png_name = name + '.png'
592 multi_line_file = os.path.join(os.path.dirname(file), png_name)
593 one_line_file = os.path.join(one_line_dir, png_name)
594 # The number of lines is determined by comparing the height of
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800595 # `multi_line_file` with `one_line_file`, where the latter is generated
596 # without the '--width' option passed to pango-view.
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800597 height = self._get_png_height(multi_line_file)
598 line_height = self._get_png_height(one_line_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800599 return int(round(height / line_height))
600
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800601 def convert_svg_to_png(self, svg_file, png_file, height, num_lines,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800602 background):
603 """Converts .svg file to .png file."""
604 background_hex = ''.join(format(x, '02x') for x in background)
605 # If the width/height of the SVG file is specified in points, the
606 # rsvg-convert command with default 90DPI will potentially cause the pixels
607 # at the right/bottom border of the output image to be transparent (or
608 # filled with the specified background color). This seems like an
609 # rsvg-convert issue regarding image scaling. Therefore, use 72DPI here
610 # to avoid the scaling.
611 command = ['rsvg-convert',
612 '--background-color', "'#%s'" % background_hex,
613 '--dpi-x', '72',
614 '--dpi-y', '72',
615 '-o', png_file]
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800616 height_px = self._to_px(height, num_lines)
Yu-Ping Wue445e042020-11-19 15:53:42 +0800617 if height_px <= 0:
618 raise BuildImageError('Height of %r <= 0 (%dpx)' %
619 (os.path.basename(svg_file), height_px))
620 command.extend(['--height', '%d' % height_px])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800621 command.append(svg_file)
622 subprocess.check_call(' '.join(command), shell=True)
623
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800624 def convert_to_bitmap(self, input_file, height, num_lines, background, output,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800625 max_colors):
626 """Converts an image file `input_file` to a BMP file `output`."""
627 image = Image.open(input_file)
628
629 # Process alpha channel and transparency.
630 if image.mode == 'RGBA':
631 target = Image.new('RGB', image.size, background)
632 image.load() # required for image.split()
633 mask = image.split()[-1]
634 target.paste(image, mask=mask)
635 elif (image.mode == 'P') and ('transparency' in image.info):
636 exit('Sorry, PNG with RGBA palette is not supported.')
637 elif image.mode != 'RGB':
638 target = image.convert('RGB')
639 else:
640 target = image
641
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800642 width_px, height_px = image.size
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800643 max_height_px = self._to_px(height, num_lines)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800644 # If the image size is larger than what will be displayed at runtime,
645 # downscale it.
646 if height_px > max_height_px:
Yu-Ping Wu354a7002021-01-07 16:07:02 +0800647 if not self.dpi_warning_printed:
648 print('Reducing effective DPI to %d, limited by screen resolution' %
649 (self.config[DPI_KEY] * max_height_px // height_px))
650 self.dpi_warning_printed = True
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800651 height_px = max_height_px
652 width_px = height_px * image.size[0] // image.size[1]
653 # Stretch image horizontally for stretched display.
Yu-Ping Wue445e042020-11-19 15:53:42 +0800654 if self.panel_stretch != 1:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800655 width_px = int(width_px * self.panel_stretch)
656 new_size = width_px, height_px
657 if new_size != image.size:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800658 target = target.resize(new_size, Image.BICUBIC)
659
660 # Export and downsample color space.
661 target.convert('P', dither=None, colors=max_colors, palette=Image.ADAPTIVE
662 ).save(output)
663
664 with open(output, 'rb+') as f:
665 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
666 f.write(bytearray([num_lines]))
667
Yu-Ping Wued95df32020-11-04 17:08:15 +0800668 def convert(self, files, output_dir, heights, max_widths, max_colors,
669 one_line_dir=None):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800670 """Converts file(s) to bitmap format."""
671 if not files:
672 raise BuildImageError('Unable to find file(s) to convert')
673
674 for file in files:
675 name, ext = os.path.splitext(os.path.basename(file))
676 output = os.path.join(output_dir, name + self.DEFAULT_OUTPUT_EXT)
677
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800678 if name in self.replace_map:
679 name = self.replace_map[name]
680 if not name:
681 continue
682 print('Replace: %s => %s' % (file, name))
683 file = os.path.join(os.path.dirname(file), name + ext)
684
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800685 background = self.BACKGROUND_COLORS.get(name, self.DEFAULT_BACKGROUND)
686 height = heights[name]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800687 max_width = max_widths[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800688
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800689 # Determine num_lines in order to scale the image
Yu-Ping Wued95df32020-11-04 17:08:15 +0800690 if one_line_dir and max_width:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800691 num_lines = self.get_num_lines(file, one_line_dir)
692 else:
693 num_lines = 1
694
695 if ext == '.svg':
696 png_file = os.path.join(self.temp_dir, name + '.png')
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800697 self.convert_svg_to_png(file, png_file, height, num_lines, background)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800698 file = png_file
699
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800700 self.convert_to_bitmap(file, height, num_lines, background, output,
701 max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800702
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800703 def convert_sprite_images(self):
704 """Converts sprite images."""
705 names = self.formats[KEY_SPRITE_FILES]
706 styles = self.formats[KEY_STYLES]
707 # Check redundant images
708 for filename in glob.glob(os.path.join(self.ASSET_DIR, SVG_FILES)):
709 name, _ = os.path.splitext(os.path.basename(filename))
710 if name not in names:
711 raise BuildImageError('Sprite image %r not specified in %s' %
712 (filename, FORMAT_FILE))
713 # Convert images
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800714 files = []
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800715 heights = {}
716 for name, category in names.items():
717 style = get_config_with_defaults(styles, category)
718 files.append(os.path.join(self.ASSET_DIR, name + '.svg'))
719 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800720 max_widths = defaultdict(lambda: None)
721 self.convert(files, self.output_dir, heights, max_widths,
722 self.ASSET_MAX_COLORS)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800723
724 def convert_generic_strings(self):
725 """Converts generic (locale-independent) strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800726 names = self.formats[KEY_GENERIC_FILES]
727 styles = self.formats[KEY_STYLES]
728 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800729 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800730 for name, category in names.items():
731 style = get_config_with_defaults(styles, category)
732 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800733 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800734
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800735 files = glob.glob(os.path.join(self.stage_dir, PNG_FILES))
Yu-Ping Wued95df32020-11-04 17:08:15 +0800736 self.convert(files, self.output_dir, heights, max_widths,
737 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800738
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800739 def _check_text_width(self, output_dir, heights, max_widths):
740 """Check if the width of text image will exceed canvas boundary."""
741 for filename in glob.glob(os.path.join(output_dir,
742 '*' + self.DEFAULT_OUTPUT_EXT)):
743 name, _ = os.path.splitext(os.path.basename(filename))
744 max_width = max_widths[name]
745 if not max_width:
746 continue
747 max_width_px = self._to_px(max_width)
748 with open(filename, 'rb') as f:
749 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
750 num_lines = f.read(1)[0]
751 height_px = self._to_px(heights[name] * num_lines)
752 with Image.open(filename) as image:
753 width_px = height_px * image.size[0] // image.size[1]
754 if width_px > max_width_px:
755 raise BuildImageError('%s: Image width %dpx greater than max width '
756 '%dpx' % (filename, width_px, max_width_px))
757
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800758 def _copy_missing_bitmaps(self):
759 """Copy missing (not yet translated) strings from locale 'en'."""
760 en_files = glob.glob(os.path.join(self.output_ro_dir, 'en',
761 '*' + self.DEFAULT_OUTPUT_EXT))
762 for locale_info in self.locales:
763 locale = locale_info.code
764 if locale == 'en':
765 continue
766 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
767 for en_file in en_files:
768 filename = os.path.basename(en_file)
769 locale_file = os.path.join(ro_locale_dir, filename)
770 if not os.path.isfile(locale_file):
771 print("WARNING: Locale '%s': copying '%s'" % (locale, filename))
772 shutil.copyfile(en_file, locale_file)
773
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800774 def convert_localized_strings(self):
775 """Converts localized strings."""
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800776 names = self.formats[KEY_LOCALIZED_FILES].copy()
777 if DIAGNOSTIC_UI:
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800778 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800779 styles = self.formats[KEY_STYLES]
780 heights = {}
Yu-Ping Wued95df32020-11-04 17:08:15 +0800781 max_widths = {}
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800782 for name, category in names.items():
783 style = get_config_with_defaults(styles, category)
784 heights[name] = style[KEY_HEIGHT]
Yu-Ping Wued95df32020-11-04 17:08:15 +0800785 max_widths[name] = style[KEY_MAX_WIDTH]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800786
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800787 # Using stderr to report progress synchronously
788 print(' processing:', end='', file=sys.stderr, flush=True)
789 for locale_info in self.locales:
790 locale = locale_info.code
791 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
792 stage_locale_dir = os.path.join(STAGE_LOCALE_DIR, locale)
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800793 print(' ' + locale, end='', file=sys.stderr, flush=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800794 os.makedirs(ro_locale_dir)
795 self.convert(
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800796 glob.glob(os.path.join(stage_locale_dir, PNG_FILES)),
Yu-Ping Wued95df32020-11-04 17:08:15 +0800797 ro_locale_dir, heights, max_widths, self.text_max_colors,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800798 one_line_dir=os.path.join(stage_locale_dir, ONE_LINE_DIR))
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800799 self._check_text_width(ro_locale_dir, heights, max_widths)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800800 print(file=sys.stderr)
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800801 self._copy_missing_bitmaps()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800802
803 def move_language_images(self):
804 """Renames language bitmaps and move to self.output_dir.
805
806 The directory self.output_dir contains locale-independent images, and is
807 used for creating vbgfx.bin by archive_images.py.
808 """
809 for locale_info in self.locales:
810 locale = locale_info.code
811 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
812 old_file = os.path.join(ro_locale_dir, 'language.bmp')
813 new_file = os.path.join(self.output_dir, 'language_%s.bmp' % locale)
814 if os.path.exists(new_file):
815 raise BuildImageError('File already exists: %s' % new_file)
816 shutil.move(old_file, new_file)
817
818 def convert_fonts(self):
819 """Converts font images"""
Yu-Ping Wu3d07a062021-01-26 18:10:32 +0800820 heights = defaultdict(lambda: DEFAULT_GLYPH_HEIGHT)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800821 max_widths = defaultdict(lambda: None)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800822 files = glob.glob(os.path.join(STAGE_FONT_DIR, SVG_FILES))
823 font_output_dir = os.path.join(self.output_dir, 'font')
824 os.makedirs(font_output_dir)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800825 self.convert(files, font_output_dir, heights, max_widths,
826 self.text_max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800827
828 def copy_images_to_rw(self):
829 """Copies localized images specified in boards.yaml for RW override."""
830 if not self.config[RW_OVERRIDE_KEY]:
831 print(' No localized images are specified for RW, skipping')
832 return
833
834 for locale_info in self.locales:
835 locale = locale_info.code
836 rw_locale_dir = os.path.join(self.output_ro_dir, locale)
837 ro_locale_dir = os.path.join(self.output_rw_dir, locale)
838 os.makedirs(rw_locale_dir)
839
840 for name in self.config[RW_OVERRIDE_KEY]:
841 ro_src = os.path.join(ro_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
842 rw_dst = os.path.join(rw_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
843 shutil.copyfile(ro_src, rw_dst)
844
845 def create_locale_list(self):
846 """Creates locale list as a CSV file.
847
848 Each line in the file is of format "code,rtl", where
849 - "code": language code of the locale
850 - "rtl": "1" for right-to-left language, "0" otherwise
851 """
852 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
853 for locale_info in self.locales:
854 f.write('{},{}\n'.format(locale_info.code,
855 int(locale_info.rtl)))
856
857 def build(self):
858 """Builds all images required by a board."""
859 # Clean up output directory
860 if os.path.exists(self.output_dir):
861 shutil.rmtree(self.output_dir)
862 os.makedirs(self.output_dir)
863
864 if not os.path.exists(self.stage_dir):
865 raise BuildImageError('Missing stage folder. Run make in strings dir.')
866
867 # Clean up temp directory
868 if os.path.exists(self.temp_dir):
869 shutil.rmtree(self.temp_dir)
870 os.makedirs(self.temp_dir)
871
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800872 print('Converting sprite images...')
873 self.convert_sprite_images()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800874
875 print('Converting generic strings...')
876 self.convert_generic_strings()
877
878 print('Converting localized strings...')
879 self.convert_localized_strings()
880
881 print('Moving language images to locale-independent directory...')
882 self.move_language_images()
883
884 print('Creating locale list file...')
885 self.create_locale_list()
886
887 print('Converting fonts...')
888 self.convert_fonts()
889
890 print('Copying specified images to RW packing directory...')
891 self.copy_images_to_rw()
892
893
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800894def main():
895 """Builds bitmaps for firmware screens."""
896 parser = argparse.ArgumentParser()
897 parser.add_argument('board', help='Target board')
898 args = parser.parse_args()
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800899 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800900
901 with open(FORMAT_FILE, encoding='utf-8') as f:
902 formats = yaml.load(f)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800903 board_config = load_boards_config(BOARDS_CONFIG_FILE)[board]
904
905 # TODO(yupingso): Put everything into Converter class
906 print('Building for ' + board)
907 build_strings(formats, board_config)
908 converter = Converter(board, formats, board_config, OUTPUT_DIR)
909 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800910
911
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800912if __name__ == '__main__':
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800913 main()