blob: 98a3f4035b31eda020823e8a173dcfbc7d491c27 [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'
26STRINGS_JSON_FILE_TMPL = '{}.json'
27FORMAT_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_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080040KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080041KEY_GENERIC_FILES = 'generic_files'
42KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080043KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
44KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080045KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080046KEY_BGCOLOR = 'bgcolor'
47KEY_FGCOLOR = 'fgcolor'
48KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080049KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080050KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080051
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080052# Board config YAML key names.
Yu-Ping Wu60b45372021-03-31 16:56:08 +080053KEY_SCREEN = 'screen'
54KEY_PANEL = 'panel'
55KEY_SDCARD = 'sdcard'
56KEY_DPI = 'dpi'
57KEY_RTL = 'rtl'
58KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080059
60BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080061
Jes Klinke1687a992020-06-16 13:47:17 -070062# Regular expressions used to eliminate spurious spaces and newlines in
63# translation strings.
64NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
65NEWLINE_REPLACEMENT = r'\1 \2'
66CRLF_PATTERN = re.compile(r'\r\n')
67MULTIBLANK_PATTERN = re.compile(r' *')
68
Yu-Ping Wu3d07a062021-01-26 18:10:32 +080069# The base for bitmap scales, same as UI_SCALE in depthcharge. For example, if
70# `SCALE_BASE` is 1000, then height = 200 means 20% of the screen height. Also
71# see the 'styles' section in format.yaml.
72SCALE_BASE = 1000
73DEFAULT_GLYPH_HEIGHT = 20
74
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +080075GLYPH_FONT = 'Cousine'
Yu-Ping Wu11027f02020-10-14 17:35:42 +080076
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080077LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080078
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080079
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080080class BuildImageError(Exception):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080081 """Exception for all errors generated during build image process."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080082
83
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080084def get_config_with_defaults(configs, key):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080085 """Gets config of `key` from `configs`.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080086
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080087 If `key` is not present in `configs`, the default config will be returned.
88 Similarly, if some config values are missing for `key`, the default ones
89 will be used.
90 """
91 config = configs[KEY_DEFAULT].copy()
92 config.update(configs.get(key, {}))
93 return config
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080094
95
Yu-Ping Wuc00e1712021-04-13 16:44:12 +080096def load_board_config(filename, board):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080097 """Loads the configuration of `board` from `filename`.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080098
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080099 Args:
100 filename: File name of a YAML config file.
101 board: Board name.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800102
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800103 Returns:
104 A dictionary mapping each board name to its config.
105 """
106 with open(filename, 'rb') as file:
107 raw = yaml.load(file)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800108
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800109 config = copy.deepcopy(raw[KEY_DEFAULT])
110 for boards, params in raw.items():
111 if boards == KEY_DEFAULT:
112 continue
113 if board not in boards.split(','):
114 continue
115 if params:
116 config.update(params)
117 break
118 else:
119 raise BuildImageError('Board config not found for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800120
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800121 return config
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800122
123
124def check_fonts(fonts):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800125 """Checks if all fonts are available."""
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800126 for locale, font in fonts.items():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800127 if subprocess.run(['fc-list', '-q', font],
128 check=False).returncode != 0:
129 raise BuildImageError('Font %r not found for locale %r' %
130 (font, locale))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800131
132
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800133def run_pango_view(input_file,
134 output_file,
135 locale,
136 font,
137 height,
138 max_width,
139 dpi,
140 bgcolor,
141 fgcolor,
142 hinting='full'):
143 """Runs pango-view."""
144 command = ['pango-view', '-q']
145 if locale:
146 command += ['--language', locale]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800147
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800148 # Font size should be proportional to the height. Here we use 2 as the
149 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
150 # will be roughly equivalent to setting the screen resolution to 1366x768.
151 font_size = height / 2
152 font_spec = '%s %r' % (font, font_size)
153 command += ['--font', font_spec]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800154
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800155 if max_width:
156 # When converting text to PNG by pango-view, the ratio of image height
157 # to the font size is usually no more than 1.1875 (with Roboto).
158 # Therefore, set the `max_width_pt` as follows to prevent UI drawing
159 # from exceeding the canvas boundary in depthcharge runtime. The divisor
160 # 2 is the same in the calculation of `font_size` above.
161 max_width_pt = int(max_width / 2 * 1.1875)
162 command.append('--width=%d' % max_width_pt)
163 if dpi:
164 command.append('--dpi=%d' % dpi)
165 command.append('--margin=0')
166 command += ['--background', bgcolor]
167 command += ['--foreground', fgcolor]
168 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800169
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800170 command += ['--output', output_file]
171 command.append(input_file)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800172
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800173 subprocess.check_call(command, stdout=subprocess.PIPE)
Yu-Ping Wu97046932021-01-25 17:38:56 +0800174
175
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800176def parse_locale_json_file(locale, json_dir):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800177 """Parses given firmware string json file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800178
179 Args:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800180 locale: The name of the locale, e.g. "da" or "pt-BR".
181 json_dir: Directory containing json output from grit.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800182
183 Returns:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800184 A dictionary for mapping of "name to content" for files to be generated.
185 """
186 result = {}
187 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
188 with open(filename, encoding='utf-8-sig') as input_file:
189 for tag, msgdict in json.load(input_file).items():
190 msgtext = msgdict['message']
191 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
192 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
193 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
194 # Strip any trailing whitespace. A trailing newline appears to make
195 # Pango report a larger layout size than what's actually visible.
196 msgtext = msgtext.strip()
197 result[tag] = msgtext
198 return result
199
200
201class Converter:
202 """Converter for converting sprites, texts, and glyphs to bitmaps.
203
204 Attributes:
205 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image
206 sprites to bitmaps.
207 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800208 """
209
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800210 # Max colors
211 SPRITE_MAX_COLORS = 128
212 GLYPH_MAX_COLORS = 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800213
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800214 def __init__(self, board, formats, board_config, output):
215 """Inits converter.
216
217 Args:
218 board: Board name.
219 formats: A dictionary of string formats.
220 board_config: A dictionary of board configurations.
221 output: Output directory.
222 """
223 self.board = board
224 self.formats = formats
225 self.config = board_config
226 self.set_dirs(output)
227 self.set_screen()
228 self.set_rename_map()
229 self.set_locales()
230 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
231
232 def set_dirs(self, output):
233 """Sets board output directory and stage directory.
234
235 Args:
236 output: Output directory.
237 """
238 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
239 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
240 self.locale_dir = os.path.join(self.strings_dir, 'locale')
241 self.output_dir = os.path.join(output, self.board)
242 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
243 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
244 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800245 self.stage_grit_dir = os.path.join(self.stage_dir, 'grit')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800246 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
247 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
248 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
249
250 def set_screen(self):
251 """Sets screen width and height."""
252 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
253
254 self.panel_stretch = fractions.Fraction(1)
255 if self.config[KEY_PANEL]:
256 # Calculate `panel_stretch`. It's used to shrink images horizontally
257 # so that the resulting images will look proportional to the
258 # original image on the stretched display. If the display is not
259 # stretched, meaning the aspect ratio is same as the screen where
260 # images were rendered, no shrinking is performed.
261 panel_width, panel_height = self.config[KEY_PANEL]
262 self.panel_stretch = fractions.Fraction(
263 self.screen_width * panel_height,
264 self.screen_height * panel_width)
265
266 if self.panel_stretch > 1:
267 raise BuildImageError(
268 'Panel aspect ratio (%f) is smaller than screen '
269 'aspect ratio (%f). It indicates screen will be '
270 'shrunk horizontally. It is currently unsupported.' %
271 (panel_width / panel_height,
272 self.screen_width / self.screen_height))
273
274 # Set up square drawing area
275 self.canvas_px = min(self.screen_width, self.screen_height)
276
277 def set_rename_map(self):
278 """Initializes a dict `self.rename_map` for image renaming.
279
280 For each items in the dict, image `key` will be renamed to `value`.
281 """
282 is_detachable = os.getenv('DETACHABLE') == '1'
283 physical_presence = os.getenv('PHYSICAL_PRESENCE')
284 rename_map = {}
285
286 # Navigation instructions
287 if is_detachable:
288 rename_map.update({
289 'nav-button_power': 'nav-key_enter',
290 'nav-button_volume_up': 'nav-key_up',
291 'nav-button_volume_down': 'nav-key_down',
292 'navigate0_tablet': 'navigate0',
293 'navigate1_tablet': 'navigate1',
294 })
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800295 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800296 rename_map.update({
297 'nav-button_power': None,
298 'nav-button_volume_up': None,
299 'nav-button_volume_down': None,
300 'navigate0_tablet': None,
301 'navigate1_tablet': None,
302 })
303
304 # Physical presence confirmation
305 if physical_presence == 'recovery':
306 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
307 rename_map['rec_to_dev_desc1_power'] = None
308 elif physical_presence == 'power':
309 rename_map['rec_to_dev_desc1_phyrec'] = None
310 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800311 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800312 rename_map['rec_to_dev_desc1_phyrec'] = None
313 rename_map['rec_to_dev_desc1_power'] = None
314 if physical_presence != 'keyboard':
315 raise BuildImageError(
316 'Invalid physical presence setting %s for board '
317 '%s' % (physical_presence, self.board))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800318
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800319 # Broken screen
320 if physical_presence == 'recovery':
321 rename_map['broken_desc_phyrec'] = 'broken_desc'
322 rename_map['broken_desc_detach'] = None
323 elif is_detachable:
324 rename_map['broken_desc_phyrec'] = None
325 rename_map['broken_desc_detach'] = 'broken_desc'
326 else:
327 rename_map['broken_desc_phyrec'] = None
328 rename_map['broken_desc_detach'] = None
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800329
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800330 # SD card
331 if not self.config[KEY_SDCARD]:
332 rename_map.update({
333 'rec_sel_desc1_no_sd':
334 'rec_sel_desc1',
335 'rec_sel_desc1_no_phone_no_sd':
336 'rec_sel_desc1_no_phone',
337 'rec_disk_step1_desc0_no_sd':
338 'rec_disk_step1_desc0',
339 })
340 else:
341 rename_map.update({
342 'rec_sel_desc1_no_sd': None,
343 'rec_sel_desc1_no_phone_no_sd': None,
344 'rec_disk_step1_desc0_no_sd': None,
345 })
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800346
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800347 # Check for duplicate new names
348 new_names = list(new_name for new_name in rename_map.values()
349 if new_name)
350 if len(set(new_names)) != len(new_names):
351 raise BuildImageError('Duplicate values found in rename_map')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800352
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800353 # Map new_name to None to skip image generation for it
354 for new_name in new_names:
355 if new_name not in rename_map:
356 rename_map[new_name] = None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800357
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800358 # Print mapping
359 print('Rename map:')
360 for name, new_name in sorted(rename_map.items()):
361 print(' %s => %s' % (name, new_name))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800362
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800363 self.rename_map = rename_map
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800364
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800365 def set_locales(self):
366 """Sets a list of locales for which localized images are converted."""
367 # LOCALES environment variable can override boards.yaml
368 env_locales = os.getenv('LOCALES')
369 rtl_locales = set(self.config[KEY_RTL])
370 if env_locales:
371 locales = env_locales.split()
372 else:
373 locales = self.config[KEY_LOCALES]
374 # Check rtl_locales are contained in locales.
375 unknown_rtl_locales = rtl_locales - set(locales)
376 if unknown_rtl_locales:
377 raise BuildImageError('Unknown locales %s in %s' %
378 (list(unknown_rtl_locales), KEY_RTL))
379 self.locales = [
380 LocaleInfo(code, code in rtl_locales) for code in locales
381 ]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800382
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800383 @classmethod
384 def get_text_colors(cls, dpi):
385 """Derives maximum text colors from `dpi`."""
386 if dpi < 64:
387 return 2
388 if dpi < 72:
389 return 3
390 if dpi < 80:
391 return 4
392 if dpi < 96:
393 return 5
394 if dpi < 112:
395 return 6
396 return 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800397
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800398 def _to_px(self, length, num_lines=1):
399 """Converts the relative coordinate to absolute one in pixels."""
400 return int(self.canvas_px * length / SCALE_BASE) * num_lines
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800401
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800402 @classmethod
403 def _get_png_height(cls, png_file):
404 # With small DPI, pango-view may generate an empty file
405 if os.path.getsize(png_file) == 0:
406 return 0
407 with Image.open(png_file) as image:
408 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800409
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800410 def get_num_lines(self, file, one_line_dir):
411 """Gets the number of lines of text in `file`."""
412 name, _ = os.path.splitext(os.path.basename(file))
413 png_name = name + '.png'
414 multi_line_file = os.path.join(os.path.dirname(file), png_name)
415 one_line_file = os.path.join(one_line_dir, png_name)
416 # The number of lines is determined by comparing the height of
417 # `multi_line_file` with `one_line_file`, where the latter is generated
418 # without the '--width' option passed to pango-view.
419 height = self._get_png_height(multi_line_file)
420 line_height = self._get_png_height(one_line_file)
421 return int(round(height / line_height))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800422
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800423 def convert_svg_to_png(self,
424 svg_file,
425 png_file,
426 height,
427 bgcolor,
428 num_lines=1):
429 """Converts SVG to PNG file."""
430 # If the width/height of the SVG file is specified in points, the
431 # rsvg-convert command with default 90DPI will potentially cause the
432 # pixels at the right/bottom border of the output image to be
433 # transparent (or filled with the specified background color). This
434 # seems like an rsvg-convert issue regarding image scaling. Therefore,
435 # use 72DPI here to avoid the scaling.
436 command = [
437 'rsvg-convert', '--background-color',
438 "'%s'" % bgcolor, '--dpi-x', '72', '--dpi-y', '72', '-o', png_file
439 ]
440 height_px = self._to_px(height, num_lines)
441 if height_px <= 0:
442 raise BuildImageError('Height of %r <= 0 (%dpx)' %
443 (os.path.basename(svg_file), height_px))
444 command.extend(['--height', '%d' % height_px])
445 command.append(svg_file)
446 subprocess.check_call(' '.join(command), shell=True)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800447
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800448 def convert_png_to_bmp(self, png_file, bmp_file, max_colors, num_lines=1):
449 """Converts PNG to BMP file."""
450 image = Image.open(png_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800451
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800452 # Process alpha channel and transparency.
453 if image.mode == 'RGBA':
454 raise BuildImageError('PNG with RGBA mode is not supported')
455 if image.mode == 'P' and 'transparency' in image.info:
456 raise BuildImageError('PNG with RGBA palette is not supported')
457 if image.mode != 'RGB':
458 target = image.convert('RGB')
459 else:
460 target = image
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800461
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800462 width_px, height_px = image.size
463 # Stretch image horizontally for stretched display.
464 if self.panel_stretch != 1:
465 width_px = int(width_px * self.panel_stretch)
466 target = target.resize((width_px, height_px), Image.BICUBIC)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800467
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800468 # Export and downsample color space.
469 target.convert('P',
470 dither=None,
471 colors=max_colors,
472 palette=Image.ADAPTIVE).save(bmp_file)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800473
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800474 with open(bmp_file, 'rb+') as f:
475 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
476 f.write(bytearray([num_lines]))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800477
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800478 @classmethod
479 def _bisect_dpi(cls, max_dpi, initial_dpi, max_height_px, get_height):
480 """Bisects to find the DPI that produces image height `max_height_px`.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800481
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800482 Args:
483 max_dpi: Maximum DPI for binary search.
484 initial_dpi: Initial DPI to try with in binary search.
485 If specified, the value must be no larger than `max_dpi`.
486 max_height_px: Maximum (target) height to search for.
487 get_height: A function converting DPI to height. The function is
488 called once before returning.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800489
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800490 Returns:
491 The best integer DPI within [1, `max_dpi`].
492 """
493 min_dpi = 1
494 first_iter = True
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800495
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800496 min_height_px = get_height(min_dpi)
497 if min_height_px > max_height_px:
498 # For some font such as "Noto Sans CJK SC", the generated height
499 # cannot go below a certain value. In this case, find max DPI with
500 # height_px <= min_height_px.
501 while min_dpi < max_dpi:
502 if first_iter and initial_dpi:
503 mid_dpi = initial_dpi
504 else:
505 mid_dpi = (min_dpi + max_dpi + 1) // 2
506 height_px = get_height(mid_dpi)
507 if height_px > min_height_px:
508 max_dpi = mid_dpi - 1
509 else:
510 min_dpi = mid_dpi
511 first_iter = False
512 get_height(max_dpi)
513 return max_dpi
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800514
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800515 # Find min DPI with height_px == max_height_px
516 while min_dpi < max_dpi:
517 if first_iter and initial_dpi:
518 mid_dpi = initial_dpi
519 else:
520 mid_dpi = (min_dpi + max_dpi) // 2
521 height_px = get_height(mid_dpi)
522 if height_px == max_height_px:
523 return mid_dpi
524 if height_px < max_height_px:
525 min_dpi = mid_dpi + 1
526 else:
527 max_dpi = mid_dpi
528 first_iter = False
529 get_height(min_dpi)
530 return min_dpi
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800531
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800532 def convert_text_to_image(self,
533 locale,
534 input_file,
535 output_file,
536 font,
537 stage_dir,
538 max_colors,
539 height=None,
540 max_width=None,
541 dpi=None,
542 initial_dpi=None,
543 bgcolor='#000000',
544 fgcolor='#ffffff',
545 use_svg=False):
546 """Converts text file `input_file` into image file.
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800547
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800548 Because pango-view does not support assigning output format options for
549 bitmap, we must create images in SVG/PNG format and then post-process
550 them (e.g. convert into BMP by ImageMagick).
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800551
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800552 Args:
553 locale: Locale (language) to select implicit rendering options. None
554 for locale-independent strings.
555 input_file: Path of input text file.
556 output_file: Path of output image file.
557 font: Font name.
558 stage_dir: Directory to store intermediate file(s).
559 max_colors: Maximum colors to convert to bitmap.
560 height: Image height relative to the screen resolution.
561 max_width: Maximum image width relative to the screen resolution.
562 dpi: DPI value passed to pango-view.
563 initial_dpi: Initial DPI to try with in binary search.
564 bgcolor: Background color (#rrggbb).
565 fgcolor: Foreground color (#rrggbb).
566 use_svg: If set to True, generate SVG file. Otherwise, generate PNG
567 file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800568
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800569 Returns:
570 Effective DPI, or `None` when not applicable.
571 """
572 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
573 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800574
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800575 name, _ = os.path.splitext(os.path.basename(input_file))
576 svg_file = os.path.join(stage_dir, name + '.svg')
577 png_file = os.path.join(stage_dir, name + '.png')
578 png_file_one_line = os.path.join(one_line_dir, name + '.png')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800579
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800580 def get_one_line_png_height(dpi):
581 """Generates a one-line PNG with `dpi` and returns its height."""
582 run_pango_view(input_file, png_file_one_line, locale, font, height,
583 0, dpi, bgcolor, fgcolor)
584 return self._get_png_height(png_file_one_line)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800585
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800586 if use_svg:
587 run_pango_view(input_file,
588 svg_file,
589 locale,
590 font,
591 height,
592 0,
593 dpi,
594 bgcolor,
595 fgcolor,
596 hinting='none')
597 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
598 self.convert_png_to_bmp(png_file, output_file, max_colors)
599 return None
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800600
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800601 if not dpi:
602 raise BuildImageError('DPI must be specified with use_svg=False')
603 eff_dpi = dpi
604 max_height_px = self._to_px(height)
605 height_px = get_one_line_png_height(dpi)
606 if height_px > max_height_px:
607 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
608 get_one_line_png_height)
609 if max_width:
610 # NOTE: With the same DPI, the height of multi-line PNG is not
611 # necessarily a multiple of the height of one-line PNG. Therefore,
612 # even with the binary search, the height of the resulting
613 # multi-line PNG might be less than "one_line_height * num_lines".
614 # We cannot binary-search DPI for multi-line PNGs because
615 # "num_lines" is dependent on DPI.
616 run_pango_view(input_file, png_file, locale, font, height,
617 max_width, eff_dpi, bgcolor, fgcolor)
618 num_lines = self.get_num_lines(png_file, one_line_dir)
619 else:
620 png_file = png_file_one_line
621 num_lines = 1
622 self.convert_png_to_bmp(png_file,
623 output_file,
624 max_colors,
625 num_lines=num_lines)
626 return eff_dpi
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800627
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800628 def convert_sprite_images(self):
629 """Converts sprite images."""
630 names = self.formats[KEY_SPRITE_FILES]
631 styles = self.formats[KEY_STYLES]
632 # Check redundant images
633 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
634 name, _ = os.path.splitext(os.path.basename(filename))
635 if name not in names:
636 raise BuildImageError('Sprite image %r not specified in %s' %
637 (filename, FORMAT_FILE))
638 # Convert images
639 for name, category in names.items():
640 new_name = self.rename_map.get(name, name)
641 if not new_name:
642 continue
643 style = get_config_with_defaults(styles, category)
644 svg_file = os.path.join(self.sprite_dir, name + '.svg')
645 png_file = os.path.join(self.temp_dir, name + '.png')
646 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
647 height = style[KEY_HEIGHT]
648 bgcolor = style[KEY_BGCOLOR]
649 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
650 self.convert_png_to_bmp(png_file, bmp_file, self.SPRITE_MAX_COLORS)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800651
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800652 def build_generic_strings(self):
653 """Builds images of generic (locale-independent) strings."""
654 dpi = self.config[KEY_DPI]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800655
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800656 names = self.formats[KEY_GENERIC_FILES]
657 styles = self.formats[KEY_STYLES]
658 fonts = self.formats[KEY_FONTS]
659 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800660
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800661 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
662 name, _ = os.path.splitext(os.path.basename(txt_file))
663 new_name = self.rename_map.get(name, name)
664 if not new_name:
665 continue
666 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
667 category = names[name]
668 style = get_config_with_defaults(styles, category)
669 if style[KEY_MAX_WIDTH]:
670 # Setting max_width causes left/right alignment of the text.
671 # However, generic strings are locale independent, and hence
672 # shouldn't have text alignment within the bitmap.
673 raise BuildImageError(
674 '{}: {!r} should be null for generic strings'.format(
675 name, KEY_MAX_WIDTH))
676 self.convert_text_to_image(None,
677 txt_file,
678 bmp_file,
679 default_font,
680 self.stage_dir,
681 self.text_max_colors,
682 height=style[KEY_HEIGHT],
683 max_width=None,
684 dpi=dpi,
685 bgcolor=style[KEY_BGCOLOR],
686 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800687
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800688 def build_locale(self, locale, names):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800689 """Builds images of strings for `locale`."""
690 dpi = self.config[KEY_DPI]
691 styles = self.formats[KEY_STYLES]
692 fonts = self.formats[KEY_FONTS]
693 font = fonts.get(locale, fonts[KEY_DEFAULT])
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800694 inputs = parse_locale_json_file(locale, self.stage_grit_dir)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800695
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800696 # Walk locale dir to add pre-generated texts such as language names.
697 for txt_file in glob.glob(
698 os.path.join(self.locale_dir, locale, '*.txt')):
699 name, _ = os.path.splitext(os.path.basename(txt_file))
700 with open(txt_file, 'r', encoding='utf-8-sig') as f:
701 inputs[name] = f.read().strip()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800702
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800703 stage_dir = os.path.join(self.stage_locale_dir, locale)
704 os.makedirs(stage_dir, exist_ok=True)
705 output_dir = os.path.join(self.output_ro_dir, locale)
706 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800707
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800708 eff_dpi_counters = defaultdict(Counter)
709 eff_dpi_counter = None
710 results = []
711 for name, category in sorted(names.items()):
712 # Ignore missing translation
713 if locale != 'en' and name not in inputs:
714 continue
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800715
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800716 new_name = self.rename_map.get(name, name)
717 if not new_name:
718 continue
719 output_file = os.path.join(output_dir, new_name + '.bmp')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800720
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800721 # Write to text file
722 text_file = os.path.join(stage_dir, name + '.txt')
723 with open(text_file, 'w', encoding='utf-8-sig') as f:
724 f.write(inputs[name] + '\n')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800725
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800726 # Convert text to image
727 style = get_config_with_defaults(styles, category)
728 height = style[KEY_HEIGHT]
729 eff_dpi_counter = eff_dpi_counters[height]
730 if eff_dpi_counter:
731 # Find the effective DPI that appears most times for `height`.
732 # This avoid doing the same binary search again and again. In
733 # case of a tie, pick the largest DPI.
734 best_eff_dpi = max(eff_dpi_counter,
735 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
736 else:
737 best_eff_dpi = None
738 eff_dpi = self.convert_text_to_image(
739 locale,
740 text_file,
741 output_file,
742 font,
743 stage_dir,
744 self.text_max_colors,
745 height=height,
746 max_width=style[KEY_MAX_WIDTH],
747 dpi=dpi,
748 initial_dpi=best_eff_dpi,
749 bgcolor=style[KEY_BGCOLOR],
750 fgcolor=style[KEY_FGCOLOR])
751 eff_dpi_counter[eff_dpi] += 1
752 assert eff_dpi <= dpi
753 if eff_dpi != dpi:
754 results.append(eff_dpi)
755 return results
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800756
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800757 def _check_text_width(self, names):
758 """Checks if text image will exceed the drawing area at runtime."""
759 styles = self.formats[KEY_STYLES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800760
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800761 for locale_info in self.locales:
762 locale = locale_info.code
763 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
764 for filename in glob.glob(os.path.join(ro_locale_dir, '*.bmp')):
765 name, _ = os.path.splitext(os.path.basename(filename))
766 category = names[name]
767 style = get_config_with_defaults(styles, category)
768 height = style[KEY_HEIGHT]
769 max_width = style[KEY_MAX_WIDTH]
770 if not max_width:
771 continue
772 max_width_px = self._to_px(max_width)
773 with open(filename, 'rb') as f:
774 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
775 num_lines = f.read(1)[0]
776 height_px = self._to_px(height * num_lines)
777 with Image.open(filename) as image:
778 width_px = height_px * image.size[0] // image.size[1]
779 if width_px > max_width_px:
780 raise BuildImageError(
781 '%s: Image width %dpx greater than max width '
782 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800783
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800784 def _copy_missing_bitmaps(self):
785 """Copies missing (not yet translated) strings from locale 'en'."""
786 en_files = glob.glob(os.path.join(self.output_ro_dir, 'en', '*.bmp'))
787 for locale_info in self.locales:
788 locale = locale_info.code
789 if locale == 'en':
790 continue
791 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
792 for en_file in en_files:
793 filename = os.path.basename(en_file)
794 locale_file = os.path.join(ro_locale_dir, filename)
795 if not os.path.isfile(locale_file):
796 print("WARNING: Locale '%s': copying '%s'" %
797 (locale, filename))
798 shutil.copyfile(en_file, locale_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800799
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800800 def build_localized_strings(self):
801 """Builds images of localized strings."""
802 # Sources are one .grd file with identifiers chosen by engineers and
803 # corresponding English texts, as well as a set of .xtb files (one for
804 # each language other than US English) with a mapping from hash to
805 # translation. Because the keys in the .xtb files are a hash of the
806 # English source text, rather than our identifiers, such as
807 # "btn_cancel", we use the "grit" command line tool to process the .grd
808 # and .xtb files, producing a set of .json files mapping our identifier
809 # to the translated string, one for every language including US English.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800810
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800811 # This invokes the grit build command to generate JSON files from the
812 # XTB files containing translations. The results are placed in
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800813 # `self.stage_grit_dir` as specified in firmware_strings.grd, i.e. one
814 # JSON file per locale.
815 os.makedirs(self.stage_grit_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800816 subprocess.check_call([
817 'grit',
818 '-i',
819 os.path.join(self.locale_dir, STRINGS_GRD_FILE),
820 'build',
821 '-o',
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800822 self.stage_grit_dir,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800823 ])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800824
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800825 # Make a copy to avoid modifying `self.formats`
826 names = copy.deepcopy(self.formats[KEY_LOCALIZED_FILES])
827 if DIAGNOSTIC_UI:
828 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800829
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800830 executor = ProcessPoolExecutor()
831 futures = []
832 for locale_info in self.locales:
833 locale = locale_info.code
834 print(locale, end=' ', flush=True)
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800835 futures.append(executor.submit(self.build_locale, locale, names))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800836
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800837 print()
838
839 try:
840 results = [future.result() for future in futures]
841 except KeyboardInterrupt:
842 executor.shutdown(wait=False)
843 sys.exit('Aborted by user')
844 else:
845 executor.shutdown()
846
847 effective_dpi = [dpi for r in results for dpi in r if dpi]
848 if effective_dpi:
849 print(
850 'Reducing effective DPI to %d, limited by screen resolution' %
851 max(effective_dpi))
852
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800853 self._check_text_width(names)
854 self._copy_missing_bitmaps()
855
856 def move_language_images(self):
857 """Renames language bitmaps and move to self.output_dir.
858
859 The directory self.output_dir contains locale-independent images, and is
860 used for creating vbgfx.bin by archive_images.py.
861 """
862 for locale_info in self.locales:
863 locale = locale_info.code
864 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
865 old_file = os.path.join(ro_locale_dir, 'language.bmp')
866 new_file = os.path.join(self.output_dir,
867 'language_%s.bmp' % locale)
868 if os.path.exists(new_file):
869 raise BuildImageError('File already exists: %s' % new_file)
870 shutil.move(old_file, new_file)
871
872 def build_glyphs(self):
873 """Builds glyphs of ascii characters."""
874 os.makedirs(self.stage_glyph_dir, exist_ok=True)
875 output_dir = os.path.join(self.output_dir, 'glyph')
876 os.makedirs(output_dir)
877 executor = ProcessPoolExecutor()
878 futures = []
879 for c in range(ord(' '), ord('~') + 1):
880 name = f'idx{c:03d}_{c:02x}'
881 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
882 with open(txt_file, 'w', encoding='ascii') as f:
883 f.write(chr(c))
884 f.write('\n')
885 output_file = os.path.join(output_dir, name + '.bmp')
886 futures.append(
887 executor.submit(self.convert_text_to_image,
888 None,
889 txt_file,
890 output_file,
891 GLYPH_FONT,
892 self.stage_glyph_dir,
893 self.GLYPH_MAX_COLORS,
894 height=DEFAULT_GLYPH_HEIGHT,
895 use_svg=True))
896 for future in futures:
897 future.result()
898 executor.shutdown()
899
900 def copy_images_to_rw(self):
901 """Copies localized images specified in boards.yaml for RW override."""
902 if not self.config[KEY_RW_OVERRIDE]:
903 print(' No localized images are specified for RW, skipping')
904 return
905
906 for locale_info in self.locales:
907 locale = locale_info.code
908 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
909 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
910 os.makedirs(rw_locale_dir)
911
912 for name in self.config[KEY_RW_OVERRIDE]:
913 ro_src = os.path.join(ro_locale_dir, name + '.bmp')
914 rw_dst = os.path.join(rw_locale_dir, name + '.bmp')
915 shutil.copyfile(ro_src, rw_dst)
916
917 def create_locale_list(self):
918 """Creates locale list as a CSV file.
919
920 Each line in the file is of format "code,rtl", where
921 - "code": language code of the locale
922 - "rtl": "1" for right-to-left language, "0" otherwise
923 """
924 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
925 for locale_info in self.locales:
926 f.write('{},{}\n'.format(locale_info.code,
927 int(locale_info.rtl)))
928
929 def build(self):
930 """Builds all images required by a board."""
931 # Clean up output directory
932 if os.path.exists(self.output_dir):
933 shutil.rmtree(self.output_dir)
934 os.makedirs(self.output_dir)
935
936 if not os.path.exists(self.stage_dir):
937 raise BuildImageError(
938 'Missing stage folder. Run make in strings dir.')
939
940 # Clean up temp directory
941 if os.path.exists(self.temp_dir):
942 shutil.rmtree(self.temp_dir)
943 os.makedirs(self.temp_dir)
944
945 print('Converting sprite images...')
946 self.convert_sprite_images()
947
948 print('Building generic strings...')
949 self.build_generic_strings()
950
951 print('Building localized strings...')
952 self.build_localized_strings()
953
954 print('Moving language images to locale-independent directory...')
955 self.move_language_images()
956
957 print('Creating locale list file...')
958 self.create_locale_list()
959
960 print('Building glyphs...')
961 self.build_glyphs()
962
963 print('Copying specified images to RW packing directory...')
964 self.copy_images_to_rw()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800965
966
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800967def main():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800968 """Builds bitmaps for firmware screens."""
969 parser = argparse.ArgumentParser()
970 parser.add_argument('board', help='Target board')
971 args = parser.parse_args()
972 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800973
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800974 with open(FORMAT_FILE, encoding='utf-8') as f:
975 formats = yaml.load(f)
976 board_config = load_board_config(BOARDS_CONFIG_FILE, board)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800977
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800978 print('Building for ' + board)
979 check_fonts(formats[KEY_FONTS])
980 print('Output dir: ' + OUTPUT_DIR)
981 converter = Converter(board, formats, board_config, OUTPUT_DIR)
982 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800983
984
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800985if __name__ == '__main__':
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800986 main()