blob: 178ac0e309fdfdda60325d009f46cba9410f7ca9 [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
Yu-Ping Wu49606eb2021-03-03 22:43:19 +08008from collections import defaultdict, namedtuple, Counter
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08009import 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
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +080013from concurrent.futures import ProcessPoolExecutor
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080014import os
15import re
Jes Klinke1687a992020-06-16 13:47:17 -070016import shutil
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080017import subprocess
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080018import sys
Hung-Te Lin04addcc2015-03-23 18:43:30 +080019
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080020import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080021from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080022
23SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080024
25STRINGS_GRD_FILE = 'firmware_strings.grd'
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +080026STRINGS_JSON_FILE_TMPL = '%s.json'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080027FORMAT_FILE = 'format.yaml'
28BOARDS_CONFIG_FILE = 'boards.yaml'
29
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080030OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080031
32ONE_LINE_DIR = 'one_line'
33SVG_FILES = '*.svg'
34PNG_FILES = '*.png'
35
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080036DIAGNOSTIC_UI = os.getenv('DIAGNOSTIC_UI') == '1'
37
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080038# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080039KEY_DEFAULT = '_DEFAULT_'
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +080040KEY_GLYPH = '_GLYPH_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080041KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080042KEY_GENERIC_FILES = 'generic_files'
43KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080044KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
45KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080046KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080047KEY_BGCOLOR = 'bgcolor'
48KEY_FGCOLOR = 'fgcolor'
49KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080050KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080051KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080052
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080053# Board config YAML key names.
Yu-Ping Wu60b45372021-03-31 16:56:08 +080054KEY_SCREEN = 'screen'
55KEY_PANEL = 'panel'
56KEY_SDCARD = 'sdcard'
57KEY_DPI = 'dpi'
58KEY_RTL = 'rtl'
59KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080060
61BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080062
Jes Klinke1687a992020-06-16 13:47:17 -070063# Regular expressions used to eliminate spurious spaces and newlines in
64# translation strings.
65NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
66NEWLINE_REPLACEMENT = r'\1 \2'
67CRLF_PATTERN = re.compile(r'\r\n')
68MULTIBLANK_PATTERN = re.compile(r' *')
69
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080070LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080071
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080072
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080073class BuildImageError(Exception):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080074 """Exception for all errors generated during build image process."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080075
76
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080077def get_config_with_defaults(configs, key):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080078 """Gets config of `key` from `configs`.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080079
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080080 If `key` is not present in `configs`, the default config will be returned.
81 Similarly, if some config values are missing for `key`, the default ones
82 will be used.
83 """
84 config = configs[KEY_DEFAULT].copy()
85 config.update(configs.get(key, {}))
86 return config
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080087
88
Yu-Ping Wuc00e1712021-04-13 16:44:12 +080089def load_board_config(filename, board):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080090 """Loads the configuration of `board` from `filename`.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080091
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080092 Args:
93 filename: File name of a YAML config file.
94 board: Board name.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080095
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080096 Returns:
97 A dictionary mapping each board name to its config.
98 """
99 with open(filename, 'rb') as file:
100 raw = yaml.load(file)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800101
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800102 config = copy.deepcopy(raw[KEY_DEFAULT])
103 for boards, params in raw.items():
104 if boards == KEY_DEFAULT:
105 continue
106 if board not in boards.split(','):
107 continue
108 if params:
109 config.update(params)
110 break
111 else:
112 raise BuildImageError('Board config not found for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800113
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800114 return config
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800115
116
117def check_fonts(fonts):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800118 """Checks if all fonts are available."""
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800119 for locale, font in fonts.items():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800120 if subprocess.run(['fc-list', '-q', font],
121 check=False).returncode != 0:
122 raise BuildImageError('Font %r not found for locale %r' %
123 (font, locale))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800124
125
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800126def run_pango_view(input_file,
127 output_file,
128 locale,
129 font,
130 height,
131 max_width,
132 dpi,
133 bgcolor,
134 fgcolor,
135 hinting='full'):
136 """Runs pango-view."""
137 command = ['pango-view', '-q']
138 if locale:
139 command += ['--language', locale]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800140
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800141 # Font size should be proportional to the height. Here we use 2 as the
142 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
143 # will be roughly equivalent to setting the screen resolution to 1366x768.
144 font_size = height / 2
145 font_spec = '%s %r' % (font, font_size)
146 command += ['--font', font_spec]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800147
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800148 if max_width:
149 # When converting text to PNG by pango-view, the ratio of image height
150 # to the font size is usually no more than 1.1875 (with Roboto).
151 # Therefore, set the `max_width_pt` as follows to prevent UI drawing
152 # from exceeding the canvas boundary in depthcharge runtime. The divisor
153 # 2 is the same in the calculation of `font_size` above.
154 max_width_pt = int(max_width / 2 * 1.1875)
155 command.append('--width=%d' % max_width_pt)
156 if dpi:
157 command.append('--dpi=%d' % dpi)
158 command.append('--margin=0')
159 command += ['--background', bgcolor]
160 command += ['--foreground', fgcolor]
161 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800162
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800163 command += ['--output', output_file]
164 command.append(input_file)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800165
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800166 subprocess.check_call(command, stdout=subprocess.PIPE)
Yu-Ping Wu97046932021-01-25 17:38:56 +0800167
168
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800169def parse_locale_json_file(locale, json_dir):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800170 """Parses given firmware string json file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800171
172 Args:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800173 locale: The name of the locale, e.g. "da" or "pt-BR".
174 json_dir: Directory containing json output from grit.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800175
176 Returns:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800177 A dictionary for mapping of "name to content" for files to be generated.
178 """
179 result = {}
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800180 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL % locale)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800181 with open(filename, encoding='utf-8-sig') as input_file:
182 for tag, msgdict in json.load(input_file).items():
183 msgtext = msgdict['message']
184 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
185 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
186 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
187 # Strip any trailing whitespace. A trailing newline appears to make
188 # Pango report a larger layout size than what's actually visible.
189 msgtext = msgtext.strip()
190 result[tag] = msgtext
191 return result
192
193
194class Converter:
195 """Converter for converting sprites, texts, and glyphs to bitmaps.
196
197 Attributes:
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800198 SCALE_BASE (int): The base for bitmap scales, same as UI_SCALE in
199 depthcharge. For example, if SCALE_BASE is 1000, then height = 200
200 means 20% of the screen height. Also see the 'styles' section in
201 format.yaml.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800202 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image
203 sprites to bitmaps.
204 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800205 """
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800206 SCALE_BASE = 1000
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800207
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800208 # Max colors
209 SPRITE_MAX_COLORS = 128
210 GLYPH_MAX_COLORS = 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800211
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800212 def __init__(self, board, formats, board_config, output):
213 """Inits converter.
214
215 Args:
216 board: Board name.
217 formats: A dictionary of string formats.
218 board_config: A dictionary of board configurations.
219 output: Output directory.
220 """
221 self.board = board
222 self.formats = formats
223 self.config = board_config
224 self.set_dirs(output)
225 self.set_screen()
226 self.set_rename_map()
227 self.set_locales()
228 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
229
230 def set_dirs(self, output):
231 """Sets board output directory and stage directory.
232
233 Args:
234 output: Output directory.
235 """
236 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
237 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
238 self.locale_dir = os.path.join(self.strings_dir, 'locale')
239 self.output_dir = os.path.join(output, self.board)
240 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
241 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
242 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800243 self.stage_grit_dir = os.path.join(self.stage_dir, 'grit')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800244 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
245 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800246 self.stage_sprite_dir = os.path.join(self.stage_dir, 'sprite')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800247
248 def set_screen(self):
249 """Sets screen width and height."""
250 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
251
252 self.panel_stretch = fractions.Fraction(1)
253 if self.config[KEY_PANEL]:
254 # Calculate `panel_stretch`. It's used to shrink images horizontally
255 # so that the resulting images will look proportional to the
256 # original image on the stretched display. If the display is not
257 # stretched, meaning the aspect ratio is same as the screen where
258 # images were rendered, no shrinking is performed.
259 panel_width, panel_height = self.config[KEY_PANEL]
260 self.panel_stretch = fractions.Fraction(
261 self.screen_width * panel_height,
262 self.screen_height * panel_width)
263
264 if self.panel_stretch > 1:
265 raise BuildImageError(
266 'Panel aspect ratio (%f) is smaller than screen '
267 'aspect ratio (%f). It indicates screen will be '
268 'shrunk horizontally. It is currently unsupported.' %
269 (panel_width / panel_height,
270 self.screen_width / self.screen_height))
271
272 # Set up square drawing area
273 self.canvas_px = min(self.screen_width, self.screen_height)
274
275 def set_rename_map(self):
276 """Initializes a dict `self.rename_map` for image renaming.
277
278 For each items in the dict, image `key` will be renamed to `value`.
279 """
280 is_detachable = os.getenv('DETACHABLE') == '1'
281 physical_presence = os.getenv('PHYSICAL_PRESENCE')
282 rename_map = {}
283
284 # Navigation instructions
285 if is_detachable:
286 rename_map.update({
287 'nav-button_power': 'nav-key_enter',
288 'nav-button_volume_up': 'nav-key_up',
289 'nav-button_volume_down': 'nav-key_down',
290 'navigate0_tablet': 'navigate0',
291 'navigate1_tablet': 'navigate1',
292 })
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800293 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800294 rename_map.update({
295 'nav-button_power': None,
296 'nav-button_volume_up': None,
297 'nav-button_volume_down': None,
298 'navigate0_tablet': None,
299 'navigate1_tablet': None,
300 })
301
302 # Physical presence confirmation
303 if physical_presence == 'recovery':
304 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
305 rename_map['rec_to_dev_desc1_power'] = None
306 elif physical_presence == 'power':
307 rename_map['rec_to_dev_desc1_phyrec'] = None
308 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800309 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800310 rename_map['rec_to_dev_desc1_phyrec'] = None
311 rename_map['rec_to_dev_desc1_power'] = None
312 if physical_presence != 'keyboard':
313 raise BuildImageError(
314 'Invalid physical presence setting %s for board '
315 '%s' % (physical_presence, self.board))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800316
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800317 # Broken screen
318 if physical_presence == 'recovery':
319 rename_map['broken_desc_phyrec'] = 'broken_desc'
320 rename_map['broken_desc_detach'] = None
321 elif is_detachable:
322 rename_map['broken_desc_phyrec'] = None
323 rename_map['broken_desc_detach'] = 'broken_desc'
324 else:
325 rename_map['broken_desc_phyrec'] = None
326 rename_map['broken_desc_detach'] = None
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800327
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800328 # SD card
329 if not self.config[KEY_SDCARD]:
330 rename_map.update({
331 'rec_sel_desc1_no_sd':
332 'rec_sel_desc1',
333 'rec_sel_desc1_no_phone_no_sd':
334 'rec_sel_desc1_no_phone',
335 'rec_disk_step1_desc0_no_sd':
336 'rec_disk_step1_desc0',
337 })
338 else:
339 rename_map.update({
340 'rec_sel_desc1_no_sd': None,
341 'rec_sel_desc1_no_phone_no_sd': None,
342 'rec_disk_step1_desc0_no_sd': None,
343 })
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800344
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800345 # Check for duplicate new names
346 new_names = list(new_name for new_name in rename_map.values()
347 if new_name)
348 if len(set(new_names)) != len(new_names):
349 raise BuildImageError('Duplicate values found in rename_map')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800350
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800351 # Map new_name to None to skip image generation for it
352 for new_name in new_names:
353 if new_name not in rename_map:
354 rename_map[new_name] = None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800355
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800356 # Print mapping
357 print('Rename map:')
358 for name, new_name in sorted(rename_map.items()):
359 print(' %s => %s' % (name, new_name))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800360
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800361 self.rename_map = rename_map
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800362
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800363 def set_locales(self):
364 """Sets a list of locales for which localized images are converted."""
365 # LOCALES environment variable can override boards.yaml
366 env_locales = os.getenv('LOCALES')
367 rtl_locales = set(self.config[KEY_RTL])
368 if env_locales:
369 locales = env_locales.split()
370 else:
371 locales = self.config[KEY_LOCALES]
372 # Check rtl_locales are contained in locales.
373 unknown_rtl_locales = rtl_locales - set(locales)
374 if unknown_rtl_locales:
375 raise BuildImageError('Unknown locales %s in %s' %
376 (list(unknown_rtl_locales), KEY_RTL))
377 self.locales = [
378 LocaleInfo(code, code in rtl_locales) for code in locales
379 ]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800380
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800381 @classmethod
382 def get_text_colors(cls, dpi):
383 """Derives maximum text colors from `dpi`."""
384 if dpi < 64:
385 return 2
386 if dpi < 72:
387 return 3
388 if dpi < 80:
389 return 4
390 if dpi < 96:
391 return 5
392 if dpi < 112:
393 return 6
394 return 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800395
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800396 def _to_px(self, length, num_lines=1):
397 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800398 return int(self.canvas_px * length / self.SCALE_BASE) * num_lines
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800399
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800400 @classmethod
401 def _get_png_height(cls, png_file):
402 # With small DPI, pango-view may generate an empty file
403 if os.path.getsize(png_file) == 0:
404 return 0
405 with Image.open(png_file) as image:
406 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800407
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800408 def get_num_lines(self, file, one_line_dir):
409 """Gets the number of lines of text in `file`."""
410 name, _ = os.path.splitext(os.path.basename(file))
411 png_name = name + '.png'
412 multi_line_file = os.path.join(os.path.dirname(file), png_name)
413 one_line_file = os.path.join(one_line_dir, png_name)
414 # The number of lines is determined by comparing the height of
415 # `multi_line_file` with `one_line_file`, where the latter is generated
416 # without the '--width' option passed to pango-view.
417 height = self._get_png_height(multi_line_file)
418 line_height = self._get_png_height(one_line_file)
419 return int(round(height / line_height))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800420
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800421 def convert_svg_to_png(self,
422 svg_file,
423 png_file,
424 height,
425 bgcolor,
426 num_lines=1):
427 """Converts SVG to PNG file."""
428 # If the width/height of the SVG file is specified in points, the
429 # rsvg-convert command with default 90DPI will potentially cause the
430 # pixels at the right/bottom border of the output image to be
431 # transparent (or filled with the specified background color). This
432 # seems like an rsvg-convert issue regarding image scaling. Therefore,
433 # use 72DPI here to avoid the scaling.
434 command = [
435 'rsvg-convert', '--background-color',
436 "'%s'" % bgcolor, '--dpi-x', '72', '--dpi-y', '72', '-o', png_file
437 ]
438 height_px = self._to_px(height, num_lines)
439 if height_px <= 0:
440 raise BuildImageError('Height of %r <= 0 (%dpx)' %
441 (os.path.basename(svg_file), height_px))
442 command.extend(['--height', '%d' % height_px])
443 command.append(svg_file)
444 subprocess.check_call(' '.join(command), shell=True)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800445
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800446 def convert_png_to_bmp(self, png_file, bmp_file, max_colors, num_lines=1):
447 """Converts PNG to BMP file."""
448 image = Image.open(png_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800449
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800450 # Process alpha channel and transparency.
451 if image.mode == 'RGBA':
452 raise BuildImageError('PNG with RGBA mode is not supported')
453 if image.mode == 'P' and 'transparency' in image.info:
454 raise BuildImageError('PNG with RGBA palette is not supported')
455 if image.mode != 'RGB':
456 target = image.convert('RGB')
457 else:
458 target = image
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800459
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800460 width_px, height_px = image.size
461 # Stretch image horizontally for stretched display.
462 if self.panel_stretch != 1:
463 width_px = int(width_px * self.panel_stretch)
464 target = target.resize((width_px, height_px), Image.BICUBIC)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800465
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800466 # Export and downsample color space.
467 target.convert('P',
468 dither=None,
469 colors=max_colors,
470 palette=Image.ADAPTIVE).save(bmp_file)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800471
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800472 with open(bmp_file, 'rb+') as f:
473 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
474 f.write(bytearray([num_lines]))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800475
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800476 @classmethod
477 def _bisect_dpi(cls, max_dpi, initial_dpi, max_height_px, get_height):
478 """Bisects to find the DPI that produces image height `max_height_px`.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800479
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800480 Args:
481 max_dpi: Maximum DPI for binary search.
482 initial_dpi: Initial DPI to try with in binary search.
483 If specified, the value must be no larger than `max_dpi`.
484 max_height_px: Maximum (target) height to search for.
485 get_height: A function converting DPI to height. The function is
486 called once before returning.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800487
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800488 Returns:
489 The best integer DPI within [1, `max_dpi`].
490 """
491 min_dpi = 1
492 first_iter = True
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800493
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800494 min_height_px = get_height(min_dpi)
495 if min_height_px > max_height_px:
496 # For some font such as "Noto Sans CJK SC", the generated height
497 # cannot go below a certain value. In this case, find max DPI with
498 # height_px <= min_height_px.
499 while min_dpi < max_dpi:
500 if first_iter and initial_dpi:
501 mid_dpi = initial_dpi
502 else:
503 mid_dpi = (min_dpi + max_dpi + 1) // 2
504 height_px = get_height(mid_dpi)
505 if height_px > min_height_px:
506 max_dpi = mid_dpi - 1
507 else:
508 min_dpi = mid_dpi
509 first_iter = False
510 get_height(max_dpi)
511 return max_dpi
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800512
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800513 # Find min DPI with height_px == max_height_px
514 while min_dpi < max_dpi:
515 if first_iter and initial_dpi:
516 mid_dpi = initial_dpi
517 else:
518 mid_dpi = (min_dpi + max_dpi) // 2
519 height_px = get_height(mid_dpi)
520 if height_px == max_height_px:
521 return mid_dpi
522 if height_px < max_height_px:
523 min_dpi = mid_dpi + 1
524 else:
525 max_dpi = mid_dpi
526 first_iter = False
527 get_height(min_dpi)
528 return min_dpi
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800529
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800530 def convert_text_to_image(self,
531 locale,
532 input_file,
533 output_file,
534 font,
535 stage_dir,
536 max_colors,
537 height=None,
538 max_width=None,
539 dpi=None,
540 initial_dpi=None,
541 bgcolor='#000000',
542 fgcolor='#ffffff',
543 use_svg=False):
544 """Converts text file `input_file` into image file.
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800545
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800546 Because pango-view does not support assigning output format options for
547 bitmap, we must create images in SVG/PNG format and then post-process
548 them (e.g. convert into BMP by ImageMagick).
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800549
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800550 Args:
551 locale: Locale (language) to select implicit rendering options. None
552 for locale-independent strings.
553 input_file: Path of input text file.
554 output_file: Path of output image file.
555 font: Font name.
556 stage_dir: Directory to store intermediate file(s).
557 max_colors: Maximum colors to convert to bitmap.
558 height: Image height relative to the screen resolution.
559 max_width: Maximum image width relative to the screen resolution.
560 dpi: DPI value passed to pango-view.
561 initial_dpi: Initial DPI to try with in binary search.
562 bgcolor: Background color (#rrggbb).
563 fgcolor: Foreground color (#rrggbb).
564 use_svg: If set to True, generate SVG file. Otherwise, generate PNG
565 file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800566
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800567 Returns:
568 Effective DPI, or `None` when not applicable.
569 """
570 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
571 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800572
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800573 name, _ = os.path.splitext(os.path.basename(input_file))
574 svg_file = os.path.join(stage_dir, name + '.svg')
575 png_file = os.path.join(stage_dir, name + '.png')
576 png_file_one_line = os.path.join(one_line_dir, name + '.png')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800577
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800578 def get_one_line_png_height(dpi):
579 """Generates a one-line PNG with `dpi` and returns its height."""
580 run_pango_view(input_file, png_file_one_line, locale, font, height,
581 0, dpi, bgcolor, fgcolor)
582 return self._get_png_height(png_file_one_line)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800583
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800584 if use_svg:
585 run_pango_view(input_file,
586 svg_file,
587 locale,
588 font,
589 height,
590 0,
591 dpi,
592 bgcolor,
593 fgcolor,
594 hinting='none')
595 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
596 self.convert_png_to_bmp(png_file, output_file, max_colors)
597 return None
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800598
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800599 if not dpi:
600 raise BuildImageError('DPI must be specified with use_svg=False')
601 eff_dpi = dpi
602 max_height_px = self._to_px(height)
603 height_px = get_one_line_png_height(dpi)
604 if height_px > max_height_px:
605 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
606 get_one_line_png_height)
607 if max_width:
608 # NOTE: With the same DPI, the height of multi-line PNG is not
609 # necessarily a multiple of the height of one-line PNG. Therefore,
610 # even with the binary search, the height of the resulting
611 # multi-line PNG might be less than "one_line_height * num_lines".
612 # We cannot binary-search DPI for multi-line PNGs because
613 # "num_lines" is dependent on DPI.
614 run_pango_view(input_file, png_file, locale, font, height,
615 max_width, eff_dpi, bgcolor, fgcolor)
616 num_lines = self.get_num_lines(png_file, one_line_dir)
617 else:
618 png_file = png_file_one_line
619 num_lines = 1
620 self.convert_png_to_bmp(png_file,
621 output_file,
622 max_colors,
623 num_lines=num_lines)
624 return eff_dpi
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800625
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800626 def convert_sprite_images(self):
627 """Converts sprite images."""
628 names = self.formats[KEY_SPRITE_FILES]
629 styles = self.formats[KEY_STYLES]
630 # Check redundant images
631 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
632 name, _ = os.path.splitext(os.path.basename(filename))
633 if name not in names:
634 raise BuildImageError('Sprite image %r not specified in %s' %
635 (filename, FORMAT_FILE))
636 # Convert images
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800637 os.makedirs(self.stage_sprite_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800638 for name, category in names.items():
639 new_name = self.rename_map.get(name, name)
640 if not new_name:
641 continue
642 style = get_config_with_defaults(styles, category)
643 svg_file = os.path.join(self.sprite_dir, name + '.svg')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800644 png_file = os.path.join(self.stage_sprite_dir, name + '.png')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800645 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
646 height = style[KEY_HEIGHT]
647 bgcolor = style[KEY_BGCOLOR]
648 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
649 self.convert_png_to_bmp(png_file, bmp_file, self.SPRITE_MAX_COLORS)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800650
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800651 def build_generic_strings(self):
652 """Builds images of generic (locale-independent) strings."""
653 dpi = self.config[KEY_DPI]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800654
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800655 names = self.formats[KEY_GENERIC_FILES]
656 styles = self.formats[KEY_STYLES]
657 fonts = self.formats[KEY_FONTS]
658 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800659
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800660 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
661 name, _ = os.path.splitext(os.path.basename(txt_file))
662 new_name = self.rename_map.get(name, name)
663 if not new_name:
664 continue
665 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
666 category = names[name]
667 style = get_config_with_defaults(styles, category)
668 if style[KEY_MAX_WIDTH]:
669 # Setting max_width causes left/right alignment of the text.
670 # However, generic strings are locale independent, and hence
671 # shouldn't have text alignment within the bitmap.
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800672 raise BuildImageError(f'{name}: {KEY_MAX_WIDTH!r} should be '
673 'null for generic strings')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800674 self.convert_text_to_image(None,
675 txt_file,
676 bmp_file,
677 default_font,
678 self.stage_dir,
679 self.text_max_colors,
680 height=style[KEY_HEIGHT],
681 max_width=None,
682 dpi=dpi,
683 bgcolor=style[KEY_BGCOLOR],
684 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800685
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800686 def build_locale(self, locale, names):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800687 """Builds images of strings for `locale`."""
688 dpi = self.config[KEY_DPI]
689 styles = self.formats[KEY_STYLES]
690 fonts = self.formats[KEY_FONTS]
691 font = fonts.get(locale, fonts[KEY_DEFAULT])
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800692 inputs = parse_locale_json_file(locale, self.stage_grit_dir)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800693
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800694 # Walk locale dir to add pre-generated texts such as language names.
695 for txt_file in glob.glob(
696 os.path.join(self.locale_dir, locale, '*.txt')):
697 name, _ = os.path.splitext(os.path.basename(txt_file))
698 with open(txt_file, 'r', encoding='utf-8-sig') as f:
699 inputs[name] = f.read().strip()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800700
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800701 stage_dir = os.path.join(self.stage_locale_dir, locale)
702 os.makedirs(stage_dir, exist_ok=True)
703 output_dir = os.path.join(self.output_ro_dir, locale)
704 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800705
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800706 eff_dpi_counters = defaultdict(Counter)
707 eff_dpi_counter = None
708 results = []
709 for name, category in sorted(names.items()):
710 # Ignore missing translation
711 if locale != 'en' and name not in inputs:
712 continue
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800713
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800714 new_name = self.rename_map.get(name, name)
715 if not new_name:
716 continue
717 output_file = os.path.join(output_dir, new_name + '.bmp')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800718
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800719 # Write to text file
720 text_file = os.path.join(stage_dir, name + '.txt')
721 with open(text_file, 'w', encoding='utf-8-sig') as f:
722 f.write(inputs[name] + '\n')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800723
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800724 # Convert text to image
725 style = get_config_with_defaults(styles, category)
726 height = style[KEY_HEIGHT]
727 eff_dpi_counter = eff_dpi_counters[height]
728 if eff_dpi_counter:
729 # Find the effective DPI that appears most times for `height`.
730 # This avoid doing the same binary search again and again. In
731 # case of a tie, pick the largest DPI.
732 best_eff_dpi = max(eff_dpi_counter,
733 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
734 else:
735 best_eff_dpi = None
736 eff_dpi = self.convert_text_to_image(
737 locale,
738 text_file,
739 output_file,
740 font,
741 stage_dir,
742 self.text_max_colors,
743 height=height,
744 max_width=style[KEY_MAX_WIDTH],
745 dpi=dpi,
746 initial_dpi=best_eff_dpi,
747 bgcolor=style[KEY_BGCOLOR],
748 fgcolor=style[KEY_FGCOLOR])
749 eff_dpi_counter[eff_dpi] += 1
750 assert eff_dpi <= dpi
751 if eff_dpi != dpi:
752 results.append(eff_dpi)
753 return results
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800754
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800755 def _check_text_width(self, names):
756 """Checks if text image will exceed the drawing area at runtime."""
757 styles = self.formats[KEY_STYLES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800758
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800759 for locale_info in self.locales:
760 locale = locale_info.code
761 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
762 for filename in glob.glob(os.path.join(ro_locale_dir, '*.bmp')):
763 name, _ = os.path.splitext(os.path.basename(filename))
764 category = names[name]
765 style = get_config_with_defaults(styles, category)
766 height = style[KEY_HEIGHT]
767 max_width = style[KEY_MAX_WIDTH]
768 if not max_width:
769 continue
770 max_width_px = self._to_px(max_width)
771 with open(filename, 'rb') as f:
772 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
773 num_lines = f.read(1)[0]
774 height_px = self._to_px(height * num_lines)
775 with Image.open(filename) as image:
776 width_px = height_px * image.size[0] // image.size[1]
777 if width_px > max_width_px:
778 raise BuildImageError(
779 '%s: Image width %dpx greater than max width '
780 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800781
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800782 def _copy_missing_bitmaps(self):
783 """Copies missing (not yet translated) strings from locale 'en'."""
784 en_files = glob.glob(os.path.join(self.output_ro_dir, 'en', '*.bmp'))
785 for locale_info in self.locales:
786 locale = locale_info.code
787 if locale == 'en':
788 continue
789 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
790 for en_file in en_files:
791 filename = os.path.basename(en_file)
792 locale_file = os.path.join(ro_locale_dir, filename)
793 if not os.path.isfile(locale_file):
794 print("WARNING: Locale '%s': copying '%s'" %
795 (locale, filename))
796 shutil.copyfile(en_file, locale_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800797
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800798 def build_localized_strings(self):
799 """Builds images of localized strings."""
800 # Sources are one .grd file with identifiers chosen by engineers and
801 # corresponding English texts, as well as a set of .xtb files (one for
802 # each language other than US English) with a mapping from hash to
803 # translation. Because the keys in the .xtb files are a hash of the
804 # English source text, rather than our identifiers, such as
805 # "btn_cancel", we use the "grit" command line tool to process the .grd
806 # and .xtb files, producing a set of .json files mapping our identifier
807 # to the translated string, one for every language including US English.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800808
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800809 # This invokes the grit build command to generate JSON files from the
810 # XTB files containing translations. The results are placed in
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800811 # `self.stage_grit_dir` as specified in firmware_strings.grd, i.e. one
812 # JSON file per locale.
813 os.makedirs(self.stage_grit_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800814 subprocess.check_call([
815 'grit',
816 '-i',
817 os.path.join(self.locale_dir, STRINGS_GRD_FILE),
818 'build',
819 '-o',
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800820 self.stage_grit_dir,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800821 ])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800822
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800823 # Make a copy to avoid modifying `self.formats`
824 names = copy.deepcopy(self.formats[KEY_LOCALIZED_FILES])
825 if DIAGNOSTIC_UI:
826 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800827
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800828 executor = ProcessPoolExecutor()
829 futures = []
830 for locale_info in self.locales:
831 locale = locale_info.code
832 print(locale, end=' ', flush=True)
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800833 futures.append(executor.submit(self.build_locale, locale, names))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800834
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800835 print()
836
837 try:
838 results = [future.result() for future in futures]
839 except KeyboardInterrupt:
840 executor.shutdown(wait=False)
841 sys.exit('Aborted by user')
842 else:
843 executor.shutdown()
844
845 effective_dpi = [dpi for r in results for dpi in r if dpi]
846 if effective_dpi:
847 print(
848 'Reducing effective DPI to %d, limited by screen resolution' %
849 max(effective_dpi))
850
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800851 self._check_text_width(names)
852 self._copy_missing_bitmaps()
853
854 def move_language_images(self):
855 """Renames language bitmaps and move to self.output_dir.
856
857 The directory self.output_dir contains locale-independent images, and is
858 used for creating vbgfx.bin by archive_images.py.
859 """
860 for locale_info in self.locales:
861 locale = locale_info.code
862 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
863 old_file = os.path.join(ro_locale_dir, 'language.bmp')
864 new_file = os.path.join(self.output_dir,
865 'language_%s.bmp' % locale)
866 if os.path.exists(new_file):
867 raise BuildImageError('File already exists: %s' % new_file)
868 shutil.move(old_file, new_file)
869
870 def build_glyphs(self):
871 """Builds glyphs of ascii characters."""
872 os.makedirs(self.stage_glyph_dir, exist_ok=True)
873 output_dir = os.path.join(self.output_dir, 'glyph')
874 os.makedirs(output_dir)
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800875 styles = self.formats[KEY_STYLES]
876 style = get_config_with_defaults(styles, KEY_GLYPH)
877 height = style[KEY_HEIGHT]
878 font = self.formats[KEY_FONTS][KEY_GLYPH]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800879 executor = ProcessPoolExecutor()
880 futures = []
881 for c in range(ord(' '), ord('~') + 1):
882 name = f'idx{c:03d}_{c:02x}'
883 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
884 with open(txt_file, 'w', encoding='ascii') as f:
885 f.write(chr(c))
886 f.write('\n')
887 output_file = os.path.join(output_dir, name + '.bmp')
888 futures.append(
889 executor.submit(self.convert_text_to_image,
890 None,
891 txt_file,
892 output_file,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800893 font,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800894 self.stage_glyph_dir,
895 self.GLYPH_MAX_COLORS,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800896 height=height,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800897 use_svg=True))
898 for future in futures:
899 future.result()
900 executor.shutdown()
901
902 def copy_images_to_rw(self):
903 """Copies localized images specified in boards.yaml for RW override."""
904 if not self.config[KEY_RW_OVERRIDE]:
905 print(' No localized images are specified for RW, skipping')
906 return
907
908 for locale_info in self.locales:
909 locale = locale_info.code
910 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
911 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
912 os.makedirs(rw_locale_dir)
913
914 for name in self.config[KEY_RW_OVERRIDE]:
915 ro_src = os.path.join(ro_locale_dir, name + '.bmp')
916 rw_dst = os.path.join(rw_locale_dir, name + '.bmp')
917 shutil.copyfile(ro_src, rw_dst)
918
919 def create_locale_list(self):
920 """Creates locale list as a CSV file.
921
922 Each line in the file is of format "code,rtl", where
923 - "code": language code of the locale
924 - "rtl": "1" for right-to-left language, "0" otherwise
925 """
926 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
927 for locale_info in self.locales:
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800928 f.write(f'{locale_info.code},{locale_info.rtl:d}\n')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800929
930 def build(self):
931 """Builds all images required by a board."""
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800932 # Clean up output/stage directories
933 for path in (self.output_dir, self.stage_dir):
934 if os.path.exists(path):
935 shutil.rmtree(path)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800936 os.makedirs(self.output_dir)
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800937 os.makedirs(self.stage_dir)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800938
939 print('Converting sprite images...')
940 self.convert_sprite_images()
941
942 print('Building generic strings...')
943 self.build_generic_strings()
944
945 print('Building localized strings...')
946 self.build_localized_strings()
947
948 print('Moving language images to locale-independent directory...')
949 self.move_language_images()
950
951 print('Creating locale list file...')
952 self.create_locale_list()
953
954 print('Building glyphs...')
955 self.build_glyphs()
956
957 print('Copying specified images to RW packing directory...')
958 self.copy_images_to_rw()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800959
960
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800961def main():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800962 """Builds bitmaps for firmware screens."""
963 parser = argparse.ArgumentParser()
964 parser.add_argument('board', help='Target board')
965 args = parser.parse_args()
966 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800967
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800968 with open(FORMAT_FILE, encoding='utf-8') as f:
969 formats = yaml.load(f)
970 board_config = load_board_config(BOARDS_CONFIG_FILE, board)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800971
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800972 print('Building for ' + board)
973 check_fonts(formats[KEY_FONTS])
974 print('Output dir: ' + OUTPUT_DIR)
975 converter = Converter(board, formats, board_config, OUTPUT_DIR)
976 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800977
978
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800979if __name__ == '__main__':
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800980 main()