blob: 52b320dd70d2744901a7a67d8135395d52ab6574 [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
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080010import glob
Jes Klinke1687a992020-06-16 13:47:17 -070011import json
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +080012from concurrent.futures import ProcessPoolExecutor
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080013import os
14import re
Jes Klinke1687a992020-06-16 13:47:17 -070015import shutil
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080016import subprocess
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080017import sys
Hung-Te Lin04addcc2015-03-23 18:43:30 +080018
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080019import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080020from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080021
22SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080023
24STRINGS_GRD_FILE = 'firmware_strings.grd'
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +080025STRINGS_JSON_FILE_TMPL = '%s.json'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080026FORMAT_FILE = 'format.yaml'
27BOARDS_CONFIG_FILE = 'boards.yaml'
28
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080029OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080030
31ONE_LINE_DIR = 'one_line'
32SVG_FILES = '*.svg'
33PNG_FILES = '*.png'
34
35# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080036KEY_DEFAULT = '_DEFAULT_'
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +080037KEY_GLYPH = '_GLYPH_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080038KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080039KEY_GENERIC_FILES = 'generic_files'
40KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080041KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080042KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080043KEY_BGCOLOR = 'bgcolor'
44KEY_FGCOLOR = 'fgcolor'
45KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080046KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080047KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080048
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080049# Board config YAML key names.
Yu-Ping Wu60b45372021-03-31 16:56:08 +080050KEY_SCREEN = 'screen'
Yu-Ping Wu60b45372021-03-31 16:56:08 +080051KEY_SDCARD = 'sdcard'
52KEY_DPI = 'dpi'
53KEY_RTL = 'rtl'
54KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080055
56BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080057
Jes Klinke1687a992020-06-16 13:47:17 -070058# Regular expressions used to eliminate spurious spaces and newlines in
59# translation strings.
60NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
61NEWLINE_REPLACEMENT = r'\1 \2'
62CRLF_PATTERN = re.compile(r'\r\n')
63MULTIBLANK_PATTERN = re.compile(r' *')
64
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080065LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080066
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080067
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080068class BuildImageError(Exception):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080069 """Exception for all errors generated during build image process."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080070
71
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080072def get_config_with_defaults(configs, key):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080073 """Gets config of `key` from `configs`.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080074
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080075 If `key` is not present in `configs`, the default config will be returned.
76 Similarly, if some config values are missing for `key`, the default ones
77 will be used.
78 """
79 config = configs[KEY_DEFAULT].copy()
80 config.update(configs.get(key, {}))
81 return config
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080082
83
Yu-Ping Wuc00e1712021-04-13 16:44:12 +080084def load_board_config(filename, board):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080085 """Loads the configuration of `board` from `filename`.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080086
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080087 Args:
88 filename: File name of a YAML config file.
89 board: Board name.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080090
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080091 Returns:
92 A dictionary mapping each board name to its config.
93 """
94 with open(filename, 'rb') as file:
95 raw = yaml.load(file)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080096
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080097 config = copy.deepcopy(raw[KEY_DEFAULT])
98 for boards, params in raw.items():
99 if boards == KEY_DEFAULT:
100 continue
101 if board not in boards.split(','):
102 continue
103 if params:
104 config.update(params)
105 break
106 else:
107 raise BuildImageError('Board config not found for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800108
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800109 return config
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800110
111
112def check_fonts(fonts):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800113 """Checks if all fonts are available."""
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800114 for locale, font in fonts.items():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800115 if subprocess.run(['fc-list', '-q', font],
116 check=False).returncode != 0:
117 raise BuildImageError('Font %r not found for locale %r' %
118 (font, locale))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800119
120
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800121def run_pango_view(input_file,
122 output_file,
123 locale,
124 font,
125 height,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800126 width_pt,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800127 dpi,
128 bgcolor,
129 fgcolor,
130 hinting='full'):
131 """Runs pango-view."""
132 command = ['pango-view', '-q']
133 if locale:
134 command += ['--language', locale]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800135
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800136 # Font size should be proportional to the height. Here we use 2 as the
137 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
138 # will be roughly equivalent to setting the screen resolution to 1366x768.
139 font_size = height / 2
140 font_spec = '%s %r' % (font, font_size)
141 command += ['--font', font_spec]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800142
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800143 if width_pt:
144 command.append('--width=%d' % width_pt)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800145 if dpi:
146 command.append('--dpi=%d' % dpi)
147 command.append('--margin=0')
148 command += ['--background', bgcolor]
149 command += ['--foreground', fgcolor]
150 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800151
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800152 command += ['--output', output_file]
153 command.append(input_file)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800154
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800155 subprocess.check_call(command, stdout=subprocess.PIPE)
Yu-Ping Wu97046932021-01-25 17:38:56 +0800156
157
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800158def parse_locale_json_file(locale, json_dir):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800159 """Parses given firmware string json file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800160
161 Args:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800162 locale: The name of the locale, e.g. "da" or "pt-BR".
163 json_dir: Directory containing json output from grit.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800164
165 Returns:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800166 A dictionary for mapping of "name to content" for files to be generated.
167 """
168 result = {}
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800169 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL % locale)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800170 with open(filename, encoding='utf-8-sig') as input_file:
171 for tag, msgdict in json.load(input_file).items():
172 msgtext = msgdict['message']
173 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
174 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
175 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
176 # Strip any trailing whitespace. A trailing newline appears to make
177 # Pango report a larger layout size than what's actually visible.
178 msgtext = msgtext.strip()
179 result[tag] = msgtext
180 return result
181
182
183class Converter:
184 """Converter for converting sprites, texts, and glyphs to bitmaps.
185
186 Attributes:
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800187 SCALE_BASE (int): The base for bitmap scales, same as UI_SCALE in
188 depthcharge. For example, if SCALE_BASE is 1000, then height = 200
189 means 20% of the screen height. Also see the 'styles' section in
190 format.yaml.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800191 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image
192 sprites to bitmaps.
193 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800194 """
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800195 SCALE_BASE = 1000
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800196
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800197 # Max colors
198 SPRITE_MAX_COLORS = 128
199 GLYPH_MAX_COLORS = 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800200
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800201 def __init__(self, board, formats, board_config, output):
202 """Inits converter.
203
204 Args:
205 board: Board name.
206 formats: A dictionary of string formats.
207 board_config: A dictionary of board configurations.
208 output: Output directory.
209 """
210 self.board = board
211 self.formats = formats
212 self.config = board_config
213 self.set_dirs(output)
214 self.set_screen()
215 self.set_rename_map()
216 self.set_locales()
217 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
218
219 def set_dirs(self, output):
220 """Sets board output directory and stage directory.
221
222 Args:
223 output: Output directory.
224 """
225 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
226 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
227 self.locale_dir = os.path.join(self.strings_dir, 'locale')
228 self.output_dir = os.path.join(output, self.board)
229 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
230 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
231 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800232 self.stage_grit_dir = os.path.join(self.stage_dir, 'grit')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800233 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
234 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800235 self.stage_sprite_dir = os.path.join(self.stage_dir, 'sprite')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800236
237 def set_screen(self):
238 """Sets screen width and height."""
239 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800240 # Set up square drawing area
241 self.canvas_px = min(self.screen_width, self.screen_height)
242
243 def set_rename_map(self):
244 """Initializes a dict `self.rename_map` for image renaming.
245
246 For each items in the dict, image `key` will be renamed to `value`.
247 """
248 is_detachable = os.getenv('DETACHABLE') == '1'
249 physical_presence = os.getenv('PHYSICAL_PRESENCE')
250 rename_map = {}
251
252 # Navigation instructions
253 if is_detachable:
254 rename_map.update({
255 'nav-button_power': 'nav-key_enter',
256 'nav-button_volume_up': 'nav-key_up',
257 'nav-button_volume_down': 'nav-key_down',
258 'navigate0_tablet': 'navigate0',
259 'navigate1_tablet': 'navigate1',
260 })
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800261 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800262 rename_map.update({
263 'nav-button_power': None,
264 'nav-button_volume_up': None,
265 'nav-button_volume_down': None,
266 'navigate0_tablet': None,
267 'navigate1_tablet': None,
268 })
269
270 # Physical presence confirmation
271 if physical_presence == 'recovery':
272 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
273 rename_map['rec_to_dev_desc1_power'] = None
274 elif physical_presence == 'power':
275 rename_map['rec_to_dev_desc1_phyrec'] = None
276 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800277 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800278 rename_map['rec_to_dev_desc1_phyrec'] = None
279 rename_map['rec_to_dev_desc1_power'] = None
280 if physical_presence != 'keyboard':
281 raise BuildImageError(
282 'Invalid physical presence setting %s for board '
283 '%s' % (physical_presence, self.board))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800284
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800285 # Broken screen
286 if physical_presence == 'recovery':
287 rename_map['broken_desc_phyrec'] = 'broken_desc'
288 rename_map['broken_desc_detach'] = None
289 elif is_detachable:
290 rename_map['broken_desc_phyrec'] = None
291 rename_map['broken_desc_detach'] = 'broken_desc'
292 else:
293 rename_map['broken_desc_phyrec'] = None
294 rename_map['broken_desc_detach'] = None
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800295
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800296 # SD card
297 if not self.config[KEY_SDCARD]:
298 rename_map.update({
299 'rec_sel_desc1_no_sd':
300 'rec_sel_desc1',
301 'rec_sel_desc1_no_phone_no_sd':
302 'rec_sel_desc1_no_phone',
303 'rec_disk_step1_desc0_no_sd':
304 'rec_disk_step1_desc0',
305 })
306 else:
307 rename_map.update({
308 'rec_sel_desc1_no_sd': None,
309 'rec_sel_desc1_no_phone_no_sd': None,
310 'rec_disk_step1_desc0_no_sd': None,
311 })
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800312
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800313 # Check for duplicate new names
314 new_names = list(new_name for new_name in rename_map.values()
315 if new_name)
316 if len(set(new_names)) != len(new_names):
317 raise BuildImageError('Duplicate values found in rename_map')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800318
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800319 # Map new_name to None to skip image generation for it
320 for new_name in new_names:
321 if new_name not in rename_map:
322 rename_map[new_name] = None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800323
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800324 # Print mapping
325 print('Rename map:')
326 for name, new_name in sorted(rename_map.items()):
327 print(' %s => %s' % (name, new_name))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800328
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800329 self.rename_map = rename_map
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800330
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800331 def set_locales(self):
332 """Sets a list of locales for which localized images are converted."""
333 # LOCALES environment variable can override boards.yaml
334 env_locales = os.getenv('LOCALES')
335 rtl_locales = set(self.config[KEY_RTL])
336 if env_locales:
337 locales = env_locales.split()
338 else:
339 locales = self.config[KEY_LOCALES]
340 # Check rtl_locales are contained in locales.
341 unknown_rtl_locales = rtl_locales - set(locales)
342 if unknown_rtl_locales:
343 raise BuildImageError('Unknown locales %s in %s' %
344 (list(unknown_rtl_locales), KEY_RTL))
345 self.locales = [
346 LocaleInfo(code, code in rtl_locales) for code in locales
347 ]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800348
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800349 @classmethod
350 def get_text_colors(cls, dpi):
351 """Derives maximum text colors from `dpi`."""
352 if dpi < 64:
353 return 2
354 if dpi < 72:
355 return 3
356 if dpi < 80:
357 return 4
358 if dpi < 96:
359 return 5
360 if dpi < 112:
361 return 6
362 return 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800363
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800364 def _to_px(self, length, num_lines=1):
365 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800366 return int(self.canvas_px * length / self.SCALE_BASE) * num_lines
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800367
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800368 def _get_runtime_width_px(self, height, num_lines, file):
369 """Gets the width in pixels `file` will be rendered at runtime."""
370 # This is different from _to_px(height, num_lines)
371 height_px = self._to_px(height * num_lines)
372 with Image.open(file) as image:
373 return height_px * image.size[0] // image.size[1]
374
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800375 @classmethod
376 def _get_png_height(cls, png_file):
377 # With small DPI, pango-view may generate an empty file
378 if os.path.getsize(png_file) == 0:
379 return 0
380 with Image.open(png_file) as image:
381 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800382
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800383 def get_num_lines(self, file, one_line_dir):
384 """Gets the number of lines of text in `file`."""
385 name, _ = os.path.splitext(os.path.basename(file))
386 png_name = name + '.png'
387 multi_line_file = os.path.join(os.path.dirname(file), png_name)
388 one_line_file = os.path.join(one_line_dir, png_name)
389 # The number of lines is determined by comparing the height of
390 # `multi_line_file` with `one_line_file`, where the latter is generated
391 # without the '--width' option passed to pango-view.
392 height = self._get_png_height(multi_line_file)
393 line_height = self._get_png_height(one_line_file)
394 return int(round(height / line_height))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800395
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800396 def convert_svg_to_png(self,
397 svg_file,
398 png_file,
399 height,
400 bgcolor,
401 num_lines=1):
402 """Converts SVG to PNG file."""
403 # If the width/height of the SVG file is specified in points, the
404 # rsvg-convert command with default 90DPI will potentially cause the
405 # pixels at the right/bottom border of the output image to be
406 # transparent (or filled with the specified background color). This
407 # seems like an rsvg-convert issue regarding image scaling. Therefore,
408 # use 72DPI here to avoid the scaling.
409 command = [
410 'rsvg-convert', '--background-color',
411 "'%s'" % bgcolor, '--dpi-x', '72', '--dpi-y', '72', '-o', png_file
412 ]
413 height_px = self._to_px(height, num_lines)
414 if height_px <= 0:
415 raise BuildImageError('Height of %r <= 0 (%dpx)' %
416 (os.path.basename(svg_file), height_px))
417 command.extend(['--height', '%d' % height_px])
418 command.append(svg_file)
419 subprocess.check_call(' '.join(command), shell=True)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800420
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800421 def convert_png_to_bmp(self, png_file, bmp_file, max_colors, num_lines=1):
422 """Converts PNG to BMP file."""
423 image = Image.open(png_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800424
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800425 # Process alpha channel and transparency.
426 if image.mode == 'RGBA':
427 raise BuildImageError('PNG with RGBA mode is not supported')
428 if image.mode == 'P' and 'transparency' in image.info:
429 raise BuildImageError('PNG with RGBA palette is not supported')
430 if image.mode != 'RGB':
Yu-Ping Wuf90b6a42021-04-23 15:24:33 +0800431 image = image.convert('RGB')
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800432
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800433 # Export and downsample color space.
Yu-Ping Wuf90b6a42021-04-23 15:24:33 +0800434 image.convert('P',
435 dither=None,
436 colors=max_colors,
437 palette=Image.ADAPTIVE).save(bmp_file)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800438
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800439 with open(bmp_file, 'rb+') as f:
440 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
441 f.write(bytearray([num_lines]))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800442
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800443 @classmethod
444 def _bisect_dpi(cls, max_dpi, initial_dpi, max_height_px, get_height):
445 """Bisects to find the DPI that produces image height `max_height_px`.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800446
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800447 Args:
448 max_dpi: Maximum DPI for binary search.
449 initial_dpi: Initial DPI to try with in binary search.
450 If specified, the value must be no larger than `max_dpi`.
451 max_height_px: Maximum (target) height to search for.
452 get_height: A function converting DPI to height. The function is
453 called once before returning.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800454
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800455 Returns:
456 The best integer DPI within [1, `max_dpi`].
457 """
458 min_dpi = 1
459 first_iter = True
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800460
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800461 min_height_px = get_height(min_dpi)
462 if min_height_px > max_height_px:
463 # For some font such as "Noto Sans CJK SC", the generated height
464 # cannot go below a certain value. In this case, find max DPI with
465 # height_px <= min_height_px.
466 while min_dpi < max_dpi:
467 if first_iter and initial_dpi:
468 mid_dpi = initial_dpi
469 else:
470 mid_dpi = (min_dpi + max_dpi + 1) // 2
471 height_px = get_height(mid_dpi)
472 if height_px > min_height_px:
473 max_dpi = mid_dpi - 1
474 else:
475 min_dpi = mid_dpi
476 first_iter = False
477 get_height(max_dpi)
478 return max_dpi
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800479
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800480 # Find min DPI with height_px == max_height_px
481 while min_dpi < max_dpi:
482 if first_iter and initial_dpi:
483 mid_dpi = initial_dpi
484 else:
485 mid_dpi = (min_dpi + max_dpi) // 2
486 height_px = get_height(mid_dpi)
487 if height_px == max_height_px:
488 return mid_dpi
489 if height_px < max_height_px:
490 min_dpi = mid_dpi + 1
491 else:
492 max_dpi = mid_dpi
493 first_iter = False
494 get_height(min_dpi)
495 return min_dpi
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800496
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800497 @classmethod
498 def _bisect_width(cls, initial_width_pt, max_width_px, get_width_px):
499 """Bisects to find the width that produces image width `max_width_px`.
500
501 Args:
502 initial_width_pt: Initial width_pt to try with in binary search.
503 max_width_px: Maximum (target) width to search for.
504 get_width_px: A function converting width_pt to width_px. The
505 function is called once before returning.
506
507 Returns:
508 The best integer width_pt.
509 """
510 min_width_pt = 1
511 width_pt = initial_width_pt
512 width_px = get_width_px(width_pt)
513 while width_px < max_width_px:
514 min_width_pt = width_pt
515 width_pt *= 2
516 width_px = get_width_px(width_pt)
517 if width_px == max_width_px:
518 return width_pt
519
520 max_width_pt = width_pt
521 # Find maximum width_pt with get_width_px(width_pt) <= max_width_px
522 while min_width_pt < max_width_pt:
523 width_pt = (min_width_pt + max_width_pt + 1) // 2
524 width_px = get_width_px(width_pt)
525 if width_px > max_width_px:
526 max_width_pt = width_pt - 1
527 else:
528 min_width_pt = width_pt
529 get_width_px(max_width_pt)
530 return max_width_pt
531
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,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800541 initial_width_pt=None,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800542 dpi=None,
543 initial_dpi=None,
544 bgcolor='#000000',
545 fgcolor='#ffffff',
546 use_svg=False):
547 """Converts text file `input_file` into image file.
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800548
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800549 Because pango-view does not support assigning output format options for
550 bitmap, we must create images in SVG/PNG format and then post-process
551 them (e.g. convert into BMP by ImageMagick).
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800552
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800553 Args:
554 locale: Locale (language) to select implicit rendering options. None
555 for locale-independent strings.
556 input_file: Path of input text file.
557 output_file: Path of output image file.
558 font: Font name.
559 stage_dir: Directory to store intermediate file(s).
560 max_colors: Maximum colors to convert to bitmap.
561 height: Image height relative to the screen resolution.
562 max_width: Maximum image width relative to the screen resolution.
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800563 initial_width_pt: Initial width_pt to try with in binary search.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800564 dpi: DPI value passed to pango-view.
565 initial_dpi: Initial DPI to try with in binary search.
566 bgcolor: Background color (#rrggbb).
567 fgcolor: Foreground color (#rrggbb).
568 use_svg: If set to True, generate SVG file. Otherwise, generate PNG
569 file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800570
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800571 Returns:
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800572 A tuple (`eff_dpi`, `width_pt`) of effective DPI and the width
573 passed to pango-view. Both `eff_dpi` and `width_pt` might be `None`
574 when not applicable.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800575 """
576 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
577 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800578
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800579 name, _ = os.path.splitext(os.path.basename(input_file))
580 svg_file = os.path.join(stage_dir, name + '.svg')
581 png_file = os.path.join(stage_dir, name + '.png')
582 png_file_one_line = os.path.join(one_line_dir, name + '.png')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800583
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800584 def get_one_line_png_height(dpi):
585 """Generates a one-line PNG with `dpi` and returns its height."""
586 run_pango_view(input_file, png_file_one_line, locale, font, height,
587 0, dpi, bgcolor, fgcolor)
588 return self._get_png_height(png_file_one_line)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800589
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800590 if use_svg:
591 run_pango_view(input_file,
592 svg_file,
593 locale,
594 font,
595 height,
596 0,
597 dpi,
598 bgcolor,
599 fgcolor,
600 hinting='none')
601 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
602 self.convert_png_to_bmp(png_file, output_file, max_colors)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800603 return None, None
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800604
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800605 if not dpi:
606 raise BuildImageError('DPI must be specified with use_svg=False')
607 eff_dpi = dpi
608 max_height_px = self._to_px(height)
609 height_px = get_one_line_png_height(dpi)
610 if height_px > max_height_px:
611 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
612 get_one_line_png_height)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800613
614 def get_width_px(width_pt):
615 run_pango_view(input_file, png_file, locale, font, height,
616 width_pt, eff_dpi, bgcolor, fgcolor)
617 num_lines = self.get_num_lines(png_file, one_line_dir)
618 return self._get_runtime_width_px(height, num_lines, png_file)
619
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800620 if max_width:
621 # NOTE: With the same DPI, the height of multi-line PNG is not
622 # necessarily a multiple of the height of one-line PNG. Therefore,
623 # even with the binary search, the height of the resulting
624 # multi-line PNG might be less than "one_line_height * num_lines".
625 # We cannot binary-search DPI for multi-line PNGs because
626 # "num_lines" is dependent on DPI.
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800627 max_width_px = self._to_px(max_width)
628 if not initial_width_pt:
629 # max_width is not in points, but this should be good enough
630 # as an initial value.
631 initial_width_pt = max_width
632 width_pt = self._bisect_width(initial_width_pt, max_width_px,
633 get_width_px)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800634 num_lines = self.get_num_lines(png_file, one_line_dir)
635 else:
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800636 width_pt = None
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800637 png_file = png_file_one_line
638 num_lines = 1
639 self.convert_png_to_bmp(png_file,
640 output_file,
641 max_colors,
642 num_lines=num_lines)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800643 return eff_dpi, width_pt
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800644
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800645 def convert_sprite_images(self):
646 """Converts sprite images."""
647 names = self.formats[KEY_SPRITE_FILES]
648 styles = self.formats[KEY_STYLES]
649 # Check redundant images
650 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
651 name, _ = os.path.splitext(os.path.basename(filename))
652 if name not in names:
653 raise BuildImageError('Sprite image %r not specified in %s' %
654 (filename, FORMAT_FILE))
655 # Convert images
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800656 os.makedirs(self.stage_sprite_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800657 for name, category in names.items():
658 new_name = self.rename_map.get(name, name)
659 if not new_name:
660 continue
661 style = get_config_with_defaults(styles, category)
662 svg_file = os.path.join(self.sprite_dir, name + '.svg')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800663 png_file = os.path.join(self.stage_sprite_dir, name + '.png')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800664 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
665 height = style[KEY_HEIGHT]
666 bgcolor = style[KEY_BGCOLOR]
667 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
668 self.convert_png_to_bmp(png_file, bmp_file, self.SPRITE_MAX_COLORS)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800669
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800670 def build_generic_strings(self):
671 """Builds images of generic (locale-independent) strings."""
672 dpi = self.config[KEY_DPI]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800673
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800674 names = self.formats[KEY_GENERIC_FILES]
675 styles = self.formats[KEY_STYLES]
676 fonts = self.formats[KEY_FONTS]
677 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800678
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800679 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
680 name, _ = os.path.splitext(os.path.basename(txt_file))
681 new_name = self.rename_map.get(name, name)
682 if not new_name:
683 continue
684 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
685 category = names[name]
686 style = get_config_with_defaults(styles, category)
687 if style[KEY_MAX_WIDTH]:
688 # Setting max_width causes left/right alignment of the text.
689 # However, generic strings are locale independent, and hence
690 # shouldn't have text alignment within the bitmap.
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800691 raise BuildImageError(f'{name}: {KEY_MAX_WIDTH!r} should be '
692 'null for generic strings')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800693 self.convert_text_to_image(None,
694 txt_file,
695 bmp_file,
696 default_font,
697 self.stage_dir,
698 self.text_max_colors,
699 height=style[KEY_HEIGHT],
700 max_width=None,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800701 initial_width_pt=None,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800702 dpi=dpi,
703 bgcolor=style[KEY_BGCOLOR],
704 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800705
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800706 def build_locale(self, locale, names):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800707 """Builds images of strings for `locale`."""
708 dpi = self.config[KEY_DPI]
709 styles = self.formats[KEY_STYLES]
710 fonts = self.formats[KEY_FONTS]
711 font = fonts.get(locale, fonts[KEY_DEFAULT])
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800712 inputs = parse_locale_json_file(locale, self.stage_grit_dir)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800713
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800714 # Walk locale dir to add pre-generated texts such as language names.
715 for txt_file in glob.glob(
716 os.path.join(self.locale_dir, locale, '*.txt')):
717 name, _ = os.path.splitext(os.path.basename(txt_file))
718 with open(txt_file, 'r', encoding='utf-8-sig') as f:
719 inputs[name] = f.read().strip()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800720
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800721 stage_dir = os.path.join(self.stage_locale_dir, locale)
722 os.makedirs(stage_dir, exist_ok=True)
723 output_dir = os.path.join(self.output_ro_dir, locale)
724 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800725
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800726 eff_dpi_counters = defaultdict(Counter)
727 eff_dpi_counter = None
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800728 width_pt_counters = defaultdict(Counter)
729 width_pt_counter = None
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800730 results = []
731 for name, category in sorted(names.items()):
Chung-Sheng Wu720e73e2021-06-22 17:53:09 +0800732 if name not in inputs:
733 raise BuildImageError(f'Locale {locale!r}: '
734 f'missing translation: {name!r}')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800735
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800736 new_name = self.rename_map.get(name, name)
737 if not new_name:
738 continue
739 output_file = os.path.join(output_dir, new_name + '.bmp')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800740
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800741 # Write to text file
742 text_file = os.path.join(stage_dir, name + '.txt')
743 with open(text_file, 'w', encoding='utf-8-sig') as f:
744 f.write(inputs[name] + '\n')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800745
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800746 # Convert text to image
747 style = get_config_with_defaults(styles, category)
748 height = style[KEY_HEIGHT]
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800749 max_width = style[KEY_MAX_WIDTH]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800750 eff_dpi_counter = eff_dpi_counters[height]
751 if eff_dpi_counter:
752 # Find the effective DPI that appears most times for `height`.
753 # This avoid doing the same binary search again and again. In
754 # case of a tie, pick the largest DPI.
755 best_eff_dpi = max(eff_dpi_counter,
756 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
757 else:
758 best_eff_dpi = None
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800759 width_pt_counter = (width_pt_counters[(height, max_width)]
760 if max_width else None)
761 if width_pt_counter:
762 # Similarly, find the most frequently used `width_pt`. In case
763 # of a tie, pick the largest width.
764 best_width_pt = max(width_pt_counter,
765 key=lambda w: (width_pt_counter[w], w))
766 else:
767 best_width_pt = None
768 eff_dpi, width_pt = self.convert_text_to_image(
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800769 locale,
770 text_file,
771 output_file,
772 font,
773 stage_dir,
774 self.text_max_colors,
775 height=height,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800776 max_width=max_width,
777 initial_width_pt=best_width_pt,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800778 dpi=dpi,
779 initial_dpi=best_eff_dpi,
780 bgcolor=style[KEY_BGCOLOR],
781 fgcolor=style[KEY_FGCOLOR])
782 eff_dpi_counter[eff_dpi] += 1
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800783 if width_pt:
784 width_pt_counter[width_pt] += 1
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800785 assert eff_dpi <= dpi
786 if eff_dpi != dpi:
787 results.append(eff_dpi)
788 return results
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800789
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800790 def _check_text_width(self, names):
791 """Checks if text image will exceed the drawing area at runtime."""
792 styles = self.formats[KEY_STYLES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800793
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800794 for locale_info in self.locales:
795 locale = locale_info.code
796 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
797 for filename in glob.glob(os.path.join(ro_locale_dir, '*.bmp')):
798 name, _ = os.path.splitext(os.path.basename(filename))
799 category = names[name]
800 style = get_config_with_defaults(styles, category)
801 height = style[KEY_HEIGHT]
802 max_width = style[KEY_MAX_WIDTH]
803 if not max_width:
804 continue
805 max_width_px = self._to_px(max_width)
806 with open(filename, 'rb') as f:
807 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
808 num_lines = f.read(1)[0]
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800809 width_px = self._get_runtime_width_px(height, num_lines,
810 filename)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800811 if width_px > max_width_px:
812 raise BuildImageError(
813 '%s: Image width %dpx greater than max width '
814 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800815
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800816 def build_localized_strings(self):
817 """Builds images of localized strings."""
818 # Sources are one .grd file with identifiers chosen by engineers and
819 # corresponding English texts, as well as a set of .xtb files (one for
820 # each language other than US English) with a mapping from hash to
821 # translation. Because the keys in the .xtb files are a hash of the
822 # English source text, rather than our identifiers, such as
823 # "btn_cancel", we use the "grit" command line tool to process the .grd
824 # and .xtb files, producing a set of .json files mapping our identifier
825 # to the translated string, one for every language including US English.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800826
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800827 # This invokes the grit build command to generate JSON files from the
828 # XTB files containing translations. The results are placed in
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800829 # `self.stage_grit_dir` as specified in firmware_strings.grd, i.e. one
830 # JSON file per locale.
831 os.makedirs(self.stage_grit_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800832 subprocess.check_call([
833 'grit',
834 '-i',
835 os.path.join(self.locale_dir, STRINGS_GRD_FILE),
836 'build',
837 '-o',
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800838 self.stage_grit_dir,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800839 ])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800840
Hsuan Ting Chen4b288402021-07-05 20:02:16 +0800841 names = self.formats[KEY_LOCALIZED_FILES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800842
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800843 executor = ProcessPoolExecutor()
844 futures = []
845 for locale_info in self.locales:
846 locale = locale_info.code
847 print(locale, end=' ', flush=True)
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800848 futures.append(executor.submit(self.build_locale, locale, names))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800849
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800850 print()
851
852 try:
853 results = [future.result() for future in futures]
854 except KeyboardInterrupt:
855 executor.shutdown(wait=False)
856 sys.exit('Aborted by user')
857 else:
858 executor.shutdown()
859
860 effective_dpi = [dpi for r in results for dpi in r if dpi]
861 if effective_dpi:
862 print(
863 'Reducing effective DPI to %d, limited by screen resolution' %
864 max(effective_dpi))
865
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800866 self._check_text_width(names)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800867
868 def move_language_images(self):
869 """Renames language bitmaps and move to self.output_dir.
870
871 The directory self.output_dir contains locale-independent images, and is
872 used for creating vbgfx.bin by archive_images.py.
873 """
874 for locale_info in self.locales:
875 locale = locale_info.code
876 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
877 old_file = os.path.join(ro_locale_dir, 'language.bmp')
878 new_file = os.path.join(self.output_dir,
879 'language_%s.bmp' % locale)
880 if os.path.exists(new_file):
881 raise BuildImageError('File already exists: %s' % new_file)
882 shutil.move(old_file, new_file)
883
884 def build_glyphs(self):
885 """Builds glyphs of ascii characters."""
886 os.makedirs(self.stage_glyph_dir, exist_ok=True)
887 output_dir = os.path.join(self.output_dir, 'glyph')
888 os.makedirs(output_dir)
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800889 styles = self.formats[KEY_STYLES]
890 style = get_config_with_defaults(styles, KEY_GLYPH)
891 height = style[KEY_HEIGHT]
892 font = self.formats[KEY_FONTS][KEY_GLYPH]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800893 executor = ProcessPoolExecutor()
894 futures = []
895 for c in range(ord(' '), ord('~') + 1):
896 name = f'idx{c:03d}_{c:02x}'
897 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
898 with open(txt_file, 'w', encoding='ascii') as f:
899 f.write(chr(c))
900 f.write('\n')
901 output_file = os.path.join(output_dir, name + '.bmp')
902 futures.append(
903 executor.submit(self.convert_text_to_image,
904 None,
905 txt_file,
906 output_file,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800907 font,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800908 self.stage_glyph_dir,
909 self.GLYPH_MAX_COLORS,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800910 height=height,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800911 use_svg=True))
912 for future in futures:
913 future.result()
914 executor.shutdown()
915
916 def copy_images_to_rw(self):
917 """Copies localized images specified in boards.yaml for RW override."""
918 if not self.config[KEY_RW_OVERRIDE]:
919 print(' No localized images are specified for RW, skipping')
920 return
921
922 for locale_info in self.locales:
923 locale = locale_info.code
924 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
925 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
926 os.makedirs(rw_locale_dir)
927
928 for name in self.config[KEY_RW_OVERRIDE]:
929 ro_src = os.path.join(ro_locale_dir, name + '.bmp')
930 rw_dst = os.path.join(rw_locale_dir, name + '.bmp')
931 shutil.copyfile(ro_src, rw_dst)
932
933 def create_locale_list(self):
934 """Creates locale list as a CSV file.
935
936 Each line in the file is of format "code,rtl", where
937 - "code": language code of the locale
938 - "rtl": "1" for right-to-left language, "0" otherwise
939 """
940 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
941 for locale_info in self.locales:
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800942 f.write(f'{locale_info.code},{locale_info.rtl:d}\n')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800943
944 def build(self):
945 """Builds all images required by a board."""
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800946 # Clean up output/stage directories
947 for path in (self.output_dir, self.stage_dir):
948 if os.path.exists(path):
949 shutil.rmtree(path)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800950 os.makedirs(self.output_dir)
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800951 os.makedirs(self.stage_dir)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800952
953 print('Converting sprite images...')
954 self.convert_sprite_images()
955
956 print('Building generic strings...')
957 self.build_generic_strings()
958
959 print('Building localized strings...')
960 self.build_localized_strings()
961
962 print('Moving language images to locale-independent directory...')
963 self.move_language_images()
964
965 print('Creating locale list file...')
966 self.create_locale_list()
967
968 print('Building glyphs...')
969 self.build_glyphs()
970
971 print('Copying specified images to RW packing directory...')
972 self.copy_images_to_rw()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800973
974
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800975def main():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800976 """Builds bitmaps for firmware screens."""
977 parser = argparse.ArgumentParser()
978 parser.add_argument('board', help='Target board')
979 args = parser.parse_args()
980 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800981
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800982 with open(FORMAT_FILE, encoding='utf-8') as f:
983 formats = yaml.load(f)
984 board_config = load_board_config(BOARDS_CONFIG_FILE, board)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800985
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800986 print('Building for ' + board)
987 check_fonts(formats[KEY_FONTS])
988 print('Output dir: ' + OUTPUT_DIR)
989 converter = Converter(board, formats, board_config, OUTPUT_DIR)
990 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800991
992
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800993if __name__ == '__main__':
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800994 main()