blob: 33cdcfac106adabc9da3bb1e82044f425a03c39e [file] [log] [blame]
Yu-Ping Wud71b4452020-06-16 11:00:26 +08001#!/usr/bin/env python
Mike Frysingerc0d0a162022-09-12 14:41:44 -04002# Copyright 2013 The ChromiumOS Authors
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08003# 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 Wubcfbcea2022-05-10 11:41:08 +08008from collections import Counter
9from collections import defaultdict
10from collections import namedtuple
11from concurrent.futures import ProcessPoolExecutor
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080012import copy
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080013import glob
Jes Klinke1687a992020-06-16 13:47:17 -070014import json
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080015import os
16import re
Jes Klinke1687a992020-06-16 13:47:17 -070017import shutil
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080018import subprocess
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080019import sys
Hung-Te Lin04addcc2015-03-23 18:43:30 +080020
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080021from PIL import Image
Yu-Ping Wubcfbcea2022-05-10 11:41:08 +080022import yaml
23
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080024
25SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080026
27STRINGS_GRD_FILE = 'firmware_strings.grd'
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +080028STRINGS_JSON_FILE_TMPL = '%s.json'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080029FORMAT_FILE = 'format.yaml'
30BOARDS_CONFIG_FILE = 'boards.yaml'
31
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080032OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080033
34ONE_LINE_DIR = 'one_line'
35SVG_FILES = '*.svg'
36PNG_FILES = '*.png'
37
38# 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_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'
Yu-Ping Wu60b45372021-03-31 16:56:08 +080054KEY_SDCARD = 'sdcard'
55KEY_DPI = 'dpi'
56KEY_RTL = 'rtl'
57KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080058
59BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080060
Jes Klinke1687a992020-06-16 13:47:17 -070061# Regular expressions used to eliminate spurious spaces and newlines in
62# translation strings.
63NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
64NEWLINE_REPLACEMENT = r'\1 \2'
65CRLF_PATTERN = re.compile(r'\r\n')
66MULTIBLANK_PATTERN = re.compile(r' *')
67
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080068LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080069
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080070
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080071class BuildImageError(Exception):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080072 """Exception for all errors generated during build image process."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080073
74
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080075def get_config_with_defaults(configs, key):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080076 """Gets config of `key` from `configs`.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080077
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080078 If `key` is not present in `configs`, the default config will be returned.
79 Similarly, if some config values are missing for `key`, the default ones
80 will be used.
81 """
82 config = configs[KEY_DEFAULT].copy()
83 config.update(configs.get(key, {}))
84 return config
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080085
86
Yu-Ping Wuc00e1712021-04-13 16:44:12 +080087def load_board_config(filename, board):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080088 """Loads the configuration of `board` from `filename`.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080089
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080090 Args:
91 filename: File name of a YAML config file.
92 board: Board name.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080093
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +080094 Returns:
95 A dictionary mapping each board name to its config.
96 """
97 with open(filename, 'rb') as file:
98 raw = yaml.load(file)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +080099
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800100 config = copy.deepcopy(raw[KEY_DEFAULT])
101 for boards, params in raw.items():
102 if boards == KEY_DEFAULT:
103 continue
104 if board not in boards.split(','):
105 continue
106 if params:
107 config.update(params)
108 break
109 else:
110 raise BuildImageError('Board config not found for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800111
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800112 return config
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800113
114
115def check_fonts(fonts):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800116 """Checks if all fonts are available."""
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800117 for locale, font in fonts.items():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800118 if subprocess.run(['fc-list', '-q', font],
119 check=False).returncode != 0:
120 raise BuildImageError('Font %r not found for locale %r' %
121 (font, locale))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800122
123
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800124def run_pango_view(input_file,
125 output_file,
126 locale,
127 font,
128 height,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800129 width_pt,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800130 dpi,
131 bgcolor,
132 fgcolor,
133 hinting='full'):
134 """Runs pango-view."""
135 command = ['pango-view', '-q']
136 if locale:
137 command += ['--language', locale]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800138
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800139 # Font size should be proportional to the height. Here we use 2 as the
140 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
141 # will be roughly equivalent to setting the screen resolution to 1366x768.
142 font_size = height / 2
143 font_spec = '%s %r' % (font, font_size)
144 command += ['--font', font_spec]
Yu-Ping Wu97046932021-01-25 17:38:56 +0800145
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800146 if width_pt:
147 command.append('--width=%d' % width_pt)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800148 if dpi:
149 command.append('--dpi=%d' % dpi)
150 command.append('--margin=0')
151 command += ['--background', bgcolor]
152 command += ['--foreground', fgcolor]
153 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800154
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800155 command += ['--output', output_file]
156 command.append(input_file)
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800157
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800158 subprocess.check_call(command, stdout=subprocess.PIPE)
Yu-Ping Wu97046932021-01-25 17:38:56 +0800159
160
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800161def parse_locale_json_file(locale, json_dir):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800162 """Parses given firmware string json file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800163
164 Args:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800165 locale: The name of the locale, e.g. "da" or "pt-BR".
166 json_dir: Directory containing json output from grit.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800167
168 Returns:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800169 A dictionary for mapping of "name to content" for files to be generated.
170 """
171 result = {}
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800172 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL % locale)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800173 with open(filename, encoding='utf-8-sig') as input_file:
174 for tag, msgdict in json.load(input_file).items():
175 msgtext = msgdict['message']
176 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
177 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
178 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
179 # Strip any trailing whitespace. A trailing newline appears to make
180 # Pango report a larger layout size than what's actually visible.
181 msgtext = msgtext.strip()
182 result[tag] = msgtext
183 return result
184
185
186class Converter:
187 """Converter for converting sprites, texts, and glyphs to bitmaps.
188
189 Attributes:
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800190 SCALE_BASE (int): The base for bitmap scales, same as UI_SCALE in
191 depthcharge. For example, if SCALE_BASE is 1000, then height = 200
192 means 20% of the screen height. Also see the 'styles' section in
193 format.yaml.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800194 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image
195 sprites to bitmaps.
196 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800197 """
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800198 SCALE_BASE = 1000
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800199
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800200 # Max colors
201 SPRITE_MAX_COLORS = 128
202 GLYPH_MAX_COLORS = 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800203
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800204 def __init__(self, board, formats, board_config, output):
205 """Inits converter.
206
207 Args:
208 board: Board name.
209 formats: A dictionary of string formats.
210 board_config: A dictionary of board configurations.
211 output: Output directory.
212 """
213 self.board = board
214 self.formats = formats
215 self.config = board_config
216 self.set_dirs(output)
217 self.set_screen()
218 self.set_rename_map()
219 self.set_locales()
220 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
221
222 def set_dirs(self, output):
223 """Sets board output directory and stage directory.
224
225 Args:
226 output: Output directory.
227 """
228 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
229 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
230 self.locale_dir = os.path.join(self.strings_dir, 'locale')
231 self.output_dir = os.path.join(output, self.board)
232 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
233 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
234 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800235 self.stage_grit_dir = os.path.join(self.stage_dir, 'grit')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800236 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
237 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800238 self.stage_sprite_dir = os.path.join(self.stage_dir, 'sprite')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800239
240 def set_screen(self):
241 """Sets screen width and height."""
242 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800243 # Set up square drawing area
244 self.canvas_px = min(self.screen_width, self.screen_height)
245
246 def set_rename_map(self):
247 """Initializes a dict `self.rename_map` for image renaming.
248
249 For each items in the dict, image `key` will be renamed to `value`.
250 """
251 is_detachable = os.getenv('DETACHABLE') == '1'
252 physical_presence = os.getenv('PHYSICAL_PRESENCE')
253 rename_map = {}
254
255 # Navigation instructions
256 if is_detachable:
257 rename_map.update({
258 'nav-button_power': 'nav-key_enter',
259 'nav-button_volume_up': 'nav-key_up',
260 'nav-button_volume_down': 'nav-key_down',
261 'navigate0_tablet': 'navigate0',
262 'navigate1_tablet': 'navigate1',
263 })
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800264 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800265 rename_map.update({
266 'nav-button_power': None,
267 'nav-button_volume_up': None,
268 'nav-button_volume_down': None,
269 'navigate0_tablet': None,
270 'navigate1_tablet': None,
271 })
272
273 # Physical presence confirmation
274 if physical_presence == 'recovery':
275 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
276 rename_map['rec_to_dev_desc1_power'] = None
277 elif physical_presence == 'power':
278 rename_map['rec_to_dev_desc1_phyrec'] = None
279 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800280 else:
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800281 rename_map['rec_to_dev_desc1_phyrec'] = None
282 rename_map['rec_to_dev_desc1_power'] = None
283 if physical_presence != 'keyboard':
284 raise BuildImageError(
285 'Invalid physical presence setting %s for board '
286 '%s' % (physical_presence, self.board))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800287
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800288 # Broken screen
289 if physical_presence == 'recovery':
290 rename_map['broken_desc_phyrec'] = 'broken_desc'
291 rename_map['broken_desc_detach'] = None
292 elif is_detachable:
293 rename_map['broken_desc_phyrec'] = None
294 rename_map['broken_desc_detach'] = 'broken_desc'
295 else:
296 rename_map['broken_desc_phyrec'] = None
297 rename_map['broken_desc_detach'] = None
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800298
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800299 # SD card
300 if not self.config[KEY_SDCARD]:
301 rename_map.update({
302 'rec_sel_desc1_no_sd':
303 'rec_sel_desc1',
304 'rec_sel_desc1_no_phone_no_sd':
305 'rec_sel_desc1_no_phone',
306 'rec_disk_step1_desc0_no_sd':
307 'rec_disk_step1_desc0',
308 })
309 else:
310 rename_map.update({
311 'rec_sel_desc1_no_sd': None,
312 'rec_sel_desc1_no_phone_no_sd': None,
313 'rec_disk_step1_desc0_no_sd': None,
314 })
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800315
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800316 # Check for duplicate new names
317 new_names = list(new_name for new_name in rename_map.values()
318 if new_name)
319 if len(set(new_names)) != len(new_names):
320 raise BuildImageError('Duplicate values found in rename_map')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800321
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800322 # Map new_name to None to skip image generation for it
323 for new_name in new_names:
324 if new_name not in rename_map:
325 rename_map[new_name] = None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800326
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800327 # Print mapping
328 print('Rename map:')
329 for name, new_name in sorted(rename_map.items()):
330 print(' %s => %s' % (name, new_name))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800331
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800332 self.rename_map = rename_map
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800333
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800334 def set_locales(self):
335 """Sets a list of locales for which localized images are converted."""
336 # LOCALES environment variable can override boards.yaml
337 env_locales = os.getenv('LOCALES')
338 rtl_locales = set(self.config[KEY_RTL])
339 if env_locales:
340 locales = env_locales.split()
341 else:
342 locales = self.config[KEY_LOCALES]
343 # Check rtl_locales are contained in locales.
344 unknown_rtl_locales = rtl_locales - set(locales)
345 if unknown_rtl_locales:
346 raise BuildImageError('Unknown locales %s in %s' %
347 (list(unknown_rtl_locales), KEY_RTL))
348 self.locales = [
349 LocaleInfo(code, code in rtl_locales) for code in locales
350 ]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800351
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800352 @classmethod
353 def get_text_colors(cls, dpi):
354 """Derives maximum text colors from `dpi`."""
355 if dpi < 64:
356 return 2
357 if dpi < 72:
358 return 3
359 if dpi < 80:
360 return 4
361 if dpi < 96:
362 return 5
363 if dpi < 112:
364 return 6
365 return 7
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800366
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800367 def _to_px(self, length, num_lines=1):
368 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800369 return int(self.canvas_px * length / self.SCALE_BASE) * num_lines
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800370
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800371 def _get_runtime_width_px(self, height, num_lines, file):
372 """Gets the width in pixels `file` will be rendered at runtime."""
373 # This is different from _to_px(height, num_lines)
374 height_px = self._to_px(height * num_lines)
375 with Image.open(file) as image:
376 return height_px * image.size[0] // image.size[1]
377
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800378 @classmethod
379 def _get_png_height(cls, png_file):
380 # With small DPI, pango-view may generate an empty file
381 if os.path.getsize(png_file) == 0:
382 return 0
383 with Image.open(png_file) as image:
384 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800385
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800386 def get_num_lines(self, file, one_line_dir):
387 """Gets the number of lines of text in `file`."""
388 name, _ = os.path.splitext(os.path.basename(file))
389 png_name = name + '.png'
390 multi_line_file = os.path.join(os.path.dirname(file), png_name)
391 one_line_file = os.path.join(one_line_dir, png_name)
392 # The number of lines is determined by comparing the height of
393 # `multi_line_file` with `one_line_file`, where the latter is generated
394 # without the '--width' option passed to pango-view.
395 height = self._get_png_height(multi_line_file)
396 line_height = self._get_png_height(one_line_file)
397 return int(round(height / line_height))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800398
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800399 def convert_svg_to_png(self,
400 svg_file,
401 png_file,
402 height,
403 bgcolor,
404 num_lines=1):
405 """Converts SVG to PNG file."""
406 # If the width/height of the SVG file is specified in points, the
407 # rsvg-convert command with default 90DPI will potentially cause the
408 # pixels at the right/bottom border of the output image to be
409 # transparent (or filled with the specified background color). This
410 # seems like an rsvg-convert issue regarding image scaling. Therefore,
411 # use 72DPI here to avoid the scaling.
412 command = [
413 'rsvg-convert', '--background-color',
414 "'%s'" % bgcolor, '--dpi-x', '72', '--dpi-y', '72', '-o', png_file
415 ]
416 height_px = self._to_px(height, num_lines)
417 if height_px <= 0:
418 raise BuildImageError('Height of %r <= 0 (%dpx)' %
419 (os.path.basename(svg_file), height_px))
420 command.extend(['--height', '%d' % height_px])
421 command.append(svg_file)
422 subprocess.check_call(' '.join(command), shell=True)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800423
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800424 def convert_png_to_bmp(self, png_file, bmp_file, max_colors, num_lines=1):
425 """Converts PNG to BMP file."""
426 image = Image.open(png_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800427
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800428 # Process alpha channel and transparency.
429 if image.mode == 'RGBA':
430 raise BuildImageError('PNG with RGBA mode is not supported')
431 if image.mode == 'P' and 'transparency' in image.info:
432 raise BuildImageError('PNG with RGBA palette is not supported')
433 if image.mode != 'RGB':
Yu-Ping Wuf90b6a42021-04-23 15:24:33 +0800434 image = image.convert('RGB')
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800435
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800436 # Export and downsample color space.
Yu-Ping Wuf90b6a42021-04-23 15:24:33 +0800437 image.convert('P',
438 dither=None,
439 colors=max_colors,
440 palette=Image.ADAPTIVE).save(bmp_file)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800441
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800442 with open(bmp_file, 'rb+') as f:
443 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
444 f.write(bytearray([num_lines]))
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800445
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800446 @classmethod
447 def _bisect_dpi(cls, max_dpi, initial_dpi, max_height_px, get_height):
448 """Bisects to find the DPI that produces image height `max_height_px`.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800449
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800450 Args:
451 max_dpi: Maximum DPI for binary search.
452 initial_dpi: Initial DPI to try with in binary search.
453 If specified, the value must be no larger than `max_dpi`.
454 max_height_px: Maximum (target) height to search for.
455 get_height: A function converting DPI to height. The function is
456 called once before returning.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800457
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800458 Returns:
459 The best integer DPI within [1, `max_dpi`].
460 """
461 min_dpi = 1
462 first_iter = True
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800463
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800464 min_height_px = get_height(min_dpi)
465 if min_height_px > max_height_px:
466 # For some font such as "Noto Sans CJK SC", the generated height
467 # cannot go below a certain value. In this case, find max DPI with
468 # height_px <= min_height_px.
469 while min_dpi < max_dpi:
470 if first_iter and initial_dpi:
471 mid_dpi = initial_dpi
472 else:
473 mid_dpi = (min_dpi + max_dpi + 1) // 2
474 height_px = get_height(mid_dpi)
475 if height_px > min_height_px:
476 max_dpi = mid_dpi - 1
477 else:
478 min_dpi = mid_dpi
479 first_iter = False
480 get_height(max_dpi)
481 return max_dpi
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800482
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800483 # Find min DPI with height_px == max_height_px
484 while min_dpi < max_dpi:
485 if first_iter and initial_dpi:
486 mid_dpi = initial_dpi
487 else:
488 mid_dpi = (min_dpi + max_dpi) // 2
489 height_px = get_height(mid_dpi)
490 if height_px == max_height_px:
491 return mid_dpi
492 if height_px < max_height_px:
493 min_dpi = mid_dpi + 1
494 else:
495 max_dpi = mid_dpi
496 first_iter = False
497 get_height(min_dpi)
498 return min_dpi
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800499
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800500 @classmethod
501 def _bisect_width(cls, initial_width_pt, max_width_px, get_width_px):
502 """Bisects to find the width that produces image width `max_width_px`.
503
504 Args:
505 initial_width_pt: Initial width_pt to try with in binary search.
506 max_width_px: Maximum (target) width to search for.
507 get_width_px: A function converting width_pt to width_px. The
508 function is called once before returning.
509
510 Returns:
511 The best integer width_pt.
512 """
513 min_width_pt = 1
514 width_pt = initial_width_pt
515 width_px = get_width_px(width_pt)
516 while width_px < max_width_px:
517 min_width_pt = width_pt
518 width_pt *= 2
519 width_px = get_width_px(width_pt)
520 if width_px == max_width_px:
521 return width_pt
522
523 max_width_pt = width_pt
524 # Find maximum width_pt with get_width_px(width_pt) <= max_width_px
525 while min_width_pt < max_width_pt:
526 width_pt = (min_width_pt + max_width_pt + 1) // 2
527 width_px = get_width_px(width_pt)
528 if width_px > max_width_px:
529 max_width_pt = width_pt - 1
530 else:
531 min_width_pt = width_pt
532 get_width_px(max_width_pt)
533 return max_width_pt
534
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800535 def convert_text_to_image(self,
536 locale,
537 input_file,
538 output_file,
539 font,
540 stage_dir,
541 max_colors,
542 height=None,
543 max_width=None,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800544 initial_width_pt=None,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800545 dpi=None,
546 initial_dpi=None,
547 bgcolor='#000000',
548 fgcolor='#ffffff',
549 use_svg=False):
550 """Converts text file `input_file` into image file.
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800551
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800552 Because pango-view does not support assigning output format options for
553 bitmap, we must create images in SVG/PNG format and then post-process
554 them (e.g. convert into BMP by ImageMagick).
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800555
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800556 Args:
557 locale: Locale (language) to select implicit rendering options. None
558 for locale-independent strings.
559 input_file: Path of input text file.
560 output_file: Path of output image file.
561 font: Font name.
562 stage_dir: Directory to store intermediate file(s).
563 max_colors: Maximum colors to convert to bitmap.
564 height: Image height relative to the screen resolution.
565 max_width: Maximum image width relative to the screen resolution.
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800566 initial_width_pt: Initial width_pt to try with in binary search.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800567 dpi: DPI value passed to pango-view.
568 initial_dpi: Initial DPI to try with in binary search.
569 bgcolor: Background color (#rrggbb).
570 fgcolor: Foreground color (#rrggbb).
571 use_svg: If set to True, generate SVG file. Otherwise, generate PNG
572 file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800573
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800574 Returns:
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800575 A tuple (`eff_dpi`, `width_pt`) of effective DPI and the width
576 passed to pango-view. Both `eff_dpi` and `width_pt` might be `None`
577 when not applicable.
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800578 """
579 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
580 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800581
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800582 name, _ = os.path.splitext(os.path.basename(input_file))
583 svg_file = os.path.join(stage_dir, name + '.svg')
584 png_file = os.path.join(stage_dir, name + '.png')
585 png_file_one_line = os.path.join(one_line_dir, name + '.png')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800586
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800587 def get_one_line_png_height(dpi):
588 """Generates a one-line PNG with `dpi` and returns its height."""
589 run_pango_view(input_file, png_file_one_line, locale, font, height,
590 0, dpi, bgcolor, fgcolor)
591 return self._get_png_height(png_file_one_line)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800592
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800593 if use_svg:
594 run_pango_view(input_file,
595 svg_file,
596 locale,
597 font,
598 height,
599 0,
600 dpi,
601 bgcolor,
602 fgcolor,
603 hinting='none')
604 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
605 self.convert_png_to_bmp(png_file, output_file, max_colors)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800606 return None, None
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800607
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800608 if not dpi:
609 raise BuildImageError('DPI must be specified with use_svg=False')
610 eff_dpi = dpi
611 max_height_px = self._to_px(height)
612 height_px = get_one_line_png_height(dpi)
613 if height_px > max_height_px:
614 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
615 get_one_line_png_height)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800616
617 def get_width_px(width_pt):
618 run_pango_view(input_file, png_file, locale, font, height,
619 width_pt, eff_dpi, bgcolor, fgcolor)
620 num_lines = self.get_num_lines(png_file, one_line_dir)
621 return self._get_runtime_width_px(height, num_lines, png_file)
622
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800623 if max_width:
624 # NOTE: With the same DPI, the height of multi-line PNG is not
625 # necessarily a multiple of the height of one-line PNG. Therefore,
626 # even with the binary search, the height of the resulting
627 # multi-line PNG might be less than "one_line_height * num_lines".
628 # We cannot binary-search DPI for multi-line PNGs because
629 # "num_lines" is dependent on DPI.
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800630 max_width_px = self._to_px(max_width)
631 if not initial_width_pt:
632 # max_width is not in points, but this should be good enough
633 # as an initial value.
634 initial_width_pt = max_width
635 width_pt = self._bisect_width(initial_width_pt, max_width_px,
636 get_width_px)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800637 num_lines = self.get_num_lines(png_file, one_line_dir)
638 else:
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800639 width_pt = None
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800640 png_file = png_file_one_line
641 num_lines = 1
642 self.convert_png_to_bmp(png_file,
643 output_file,
644 max_colors,
645 num_lines=num_lines)
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800646 return eff_dpi, width_pt
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800647
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800648 def convert_sprite_images(self):
649 """Converts sprite images."""
650 names = self.formats[KEY_SPRITE_FILES]
651 styles = self.formats[KEY_STYLES]
652 # Check redundant images
653 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
654 name, _ = os.path.splitext(os.path.basename(filename))
655 if name not in names:
656 raise BuildImageError('Sprite image %r not specified in %s' %
657 (filename, FORMAT_FILE))
658 # Convert images
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800659 os.makedirs(self.stage_sprite_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800660 for name, category in names.items():
661 new_name = self.rename_map.get(name, name)
662 if not new_name:
663 continue
664 style = get_config_with_defaults(styles, category)
665 svg_file = os.path.join(self.sprite_dir, name + '.svg')
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800666 png_file = os.path.join(self.stage_sprite_dir, name + '.png')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800667 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
668 height = style[KEY_HEIGHT]
669 bgcolor = style[KEY_BGCOLOR]
670 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
671 self.convert_png_to_bmp(png_file, bmp_file, self.SPRITE_MAX_COLORS)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800672
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800673 def build_generic_strings(self):
674 """Builds images of generic (locale-independent) strings."""
675 dpi = self.config[KEY_DPI]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800676
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800677 names = self.formats[KEY_GENERIC_FILES]
678 styles = self.formats[KEY_STYLES]
679 fonts = self.formats[KEY_FONTS]
680 default_font = fonts[KEY_DEFAULT]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800681
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800682 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
683 name, _ = os.path.splitext(os.path.basename(txt_file))
684 new_name = self.rename_map.get(name, name)
685 if not new_name:
686 continue
687 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
688 category = names[name]
689 style = get_config_with_defaults(styles, category)
690 if style[KEY_MAX_WIDTH]:
691 # Setting max_width causes left/right alignment of the text.
692 # However, generic strings are locale independent, and hence
693 # shouldn't have text alignment within the bitmap.
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800694 raise BuildImageError(f'{name}: {KEY_MAX_WIDTH!r} should be '
695 'null for generic strings')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800696 self.convert_text_to_image(None,
697 txt_file,
698 bmp_file,
699 default_font,
700 self.stage_dir,
701 self.text_max_colors,
702 height=style[KEY_HEIGHT],
703 max_width=None,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800704 initial_width_pt=None,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800705 dpi=dpi,
706 bgcolor=style[KEY_BGCOLOR],
707 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800708
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800709 def build_locale(self, locale, names):
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800710 """Builds images of strings for `locale`."""
711 dpi = self.config[KEY_DPI]
712 styles = self.formats[KEY_STYLES]
713 fonts = self.formats[KEY_FONTS]
714 font = fonts.get(locale, fonts[KEY_DEFAULT])
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800715 inputs = parse_locale_json_file(locale, self.stage_grit_dir)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800716
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800717 # Walk locale dir to add pre-generated texts such as language names.
718 for txt_file in glob.glob(
719 os.path.join(self.locale_dir, locale, '*.txt')):
720 name, _ = os.path.splitext(os.path.basename(txt_file))
721 with open(txt_file, 'r', encoding='utf-8-sig') as f:
722 inputs[name] = f.read().strip()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800723
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800724 stage_dir = os.path.join(self.stage_locale_dir, locale)
725 os.makedirs(stage_dir, exist_ok=True)
726 output_dir = os.path.join(self.output_ro_dir, locale)
727 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800728
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800729 eff_dpi_counters = defaultdict(Counter)
730 eff_dpi_counter = None
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800731 width_pt_counters = defaultdict(Counter)
732 width_pt_counter = None
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800733 results = []
734 for name, category in sorted(names.items()):
Chung-Sheng Wu720e73e2021-06-22 17:53:09 +0800735 if name not in inputs:
736 raise BuildImageError(f'Locale {locale!r}: '
737 f'missing translation: {name!r}')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800738
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800739 new_name = self.rename_map.get(name, name)
740 if not new_name:
741 continue
742 output_file = os.path.join(output_dir, new_name + '.bmp')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800743
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800744 # Write to text file
745 text_file = os.path.join(stage_dir, name + '.txt')
746 with open(text_file, 'w', encoding='utf-8-sig') as f:
747 f.write(inputs[name] + '\n')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800748
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800749 # Convert text to image
750 style = get_config_with_defaults(styles, category)
751 height = style[KEY_HEIGHT]
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800752 max_width = style[KEY_MAX_WIDTH]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800753 eff_dpi_counter = eff_dpi_counters[height]
754 if eff_dpi_counter:
755 # Find the effective DPI that appears most times for `height`.
756 # This avoid doing the same binary search again and again. In
757 # case of a tie, pick the largest DPI.
758 best_eff_dpi = max(eff_dpi_counter,
759 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
760 else:
761 best_eff_dpi = None
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800762 width_pt_counter = (width_pt_counters[(height, max_width)]
763 if max_width else None)
764 if width_pt_counter:
765 # Similarly, find the most frequently used `width_pt`. In case
766 # of a tie, pick the largest width.
767 best_width_pt = max(width_pt_counter,
768 key=lambda w: (width_pt_counter[w], w))
769 else:
770 best_width_pt = None
771 eff_dpi, width_pt = self.convert_text_to_image(
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800772 locale,
773 text_file,
774 output_file,
775 font,
776 stage_dir,
777 self.text_max_colors,
778 height=height,
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800779 max_width=max_width,
780 initial_width_pt=best_width_pt,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800781 dpi=dpi,
782 initial_dpi=best_eff_dpi,
783 bgcolor=style[KEY_BGCOLOR],
784 fgcolor=style[KEY_FGCOLOR])
785 eff_dpi_counter[eff_dpi] += 1
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800786 if width_pt:
787 width_pt_counter[width_pt] += 1
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800788 assert eff_dpi <= dpi
789 if eff_dpi != dpi:
790 results.append(eff_dpi)
791 return results
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800792
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800793 def _check_text_width(self, names):
794 """Checks if text image will exceed the drawing area at runtime."""
795 styles = self.formats[KEY_STYLES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800796
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800797 for locale_info in self.locales:
798 locale = locale_info.code
799 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
800 for filename in glob.glob(os.path.join(ro_locale_dir, '*.bmp')):
801 name, _ = os.path.splitext(os.path.basename(filename))
802 category = names[name]
803 style = get_config_with_defaults(styles, category)
804 height = style[KEY_HEIGHT]
805 max_width = style[KEY_MAX_WIDTH]
806 if not max_width:
807 continue
808 max_width_px = self._to_px(max_width)
809 with open(filename, 'rb') as f:
810 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
811 num_lines = f.read(1)[0]
Yu-Ping Wu2008cb12021-09-14 13:53:24 +0800812 width_px = self._get_runtime_width_px(height, num_lines,
813 filename)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800814 if width_px > max_width_px:
815 raise BuildImageError(
816 '%s: Image width %dpx greater than max width '
817 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800818
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800819 def build_localized_strings(self):
820 """Builds images of localized strings."""
821 # Sources are one .grd file with identifiers chosen by engineers and
822 # corresponding English texts, as well as a set of .xtb files (one for
823 # each language other than US English) with a mapping from hash to
824 # translation. Because the keys in the .xtb files are a hash of the
825 # English source text, rather than our identifiers, such as
826 # "btn_cancel", we use the "grit" command line tool to process the .grd
827 # and .xtb files, producing a set of .json files mapping our identifier
828 # to the translated string, one for every language including US English.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800829
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800830 # This invokes the grit build command to generate JSON files from the
831 # XTB files containing translations. The results are placed in
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800832 # `self.stage_grit_dir` as specified in firmware_strings.grd, i.e. one
833 # JSON file per locale.
834 os.makedirs(self.stage_grit_dir, exist_ok=True)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800835 subprocess.check_call([
836 'grit',
837 '-i',
838 os.path.join(self.locale_dir, STRINGS_GRD_FILE),
839 'build',
840 '-o',
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800841 self.stage_grit_dir,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800842 ])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800843
Hsuan Ting Chen4b288402021-07-05 20:02:16 +0800844 names = self.formats[KEY_LOCALIZED_FILES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800845
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800846 executor = ProcessPoolExecutor()
847 futures = []
848 for locale_info in self.locales:
849 locale = locale_info.code
850 print(locale, end=' ', flush=True)
Yu-Ping Wu08f607b2021-04-20 13:11:37 +0800851 futures.append(executor.submit(self.build_locale, locale, names))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800852
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800853 print()
854
855 try:
856 results = [future.result() for future in futures]
857 except KeyboardInterrupt:
858 executor.shutdown(wait=False)
859 sys.exit('Aborted by user')
860 else:
861 executor.shutdown()
862
863 effective_dpi = [dpi for r in results for dpi in r if dpi]
864 if effective_dpi:
865 print(
866 'Reducing effective DPI to %d, limited by screen resolution' %
867 max(effective_dpi))
868
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800869 self._check_text_width(names)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800870
871 def move_language_images(self):
872 """Renames language bitmaps and move to self.output_dir.
873
874 The directory self.output_dir contains locale-independent images, and is
875 used for creating vbgfx.bin by archive_images.py.
876 """
877 for locale_info in self.locales:
878 locale = locale_info.code
879 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
880 old_file = os.path.join(ro_locale_dir, 'language.bmp')
881 new_file = os.path.join(self.output_dir,
882 'language_%s.bmp' % locale)
883 if os.path.exists(new_file):
884 raise BuildImageError('File already exists: %s' % new_file)
885 shutil.move(old_file, new_file)
886
887 def build_glyphs(self):
888 """Builds glyphs of ascii characters."""
889 os.makedirs(self.stage_glyph_dir, exist_ok=True)
890 output_dir = os.path.join(self.output_dir, 'glyph')
891 os.makedirs(output_dir)
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800892 styles = self.formats[KEY_STYLES]
893 style = get_config_with_defaults(styles, KEY_GLYPH)
894 height = style[KEY_HEIGHT]
895 font = self.formats[KEY_FONTS][KEY_GLYPH]
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800896 executor = ProcessPoolExecutor()
897 futures = []
898 for c in range(ord(' '), ord('~') + 1):
899 name = f'idx{c:03d}_{c:02x}'
900 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
901 with open(txt_file, 'w', encoding='ascii') as f:
902 f.write(chr(c))
903 f.write('\n')
904 output_file = os.path.join(output_dir, name + '.bmp')
905 futures.append(
906 executor.submit(self.convert_text_to_image,
907 None,
908 txt_file,
909 output_file,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800910 font,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800911 self.stage_glyph_dir,
912 self.GLYPH_MAX_COLORS,
Yu-Ping Wu4c723ea2021-04-20 13:20:35 +0800913 height=height,
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800914 use_svg=True))
915 for future in futures:
916 future.result()
917 executor.shutdown()
918
919 def copy_images_to_rw(self):
920 """Copies localized images specified in boards.yaml for RW override."""
921 if not self.config[KEY_RW_OVERRIDE]:
922 print(' No localized images are specified for RW, skipping')
923 return
924
925 for locale_info in self.locales:
926 locale = locale_info.code
927 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
928 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
929 os.makedirs(rw_locale_dir)
930
931 for name in self.config[KEY_RW_OVERRIDE]:
932 ro_src = os.path.join(ro_locale_dir, name + '.bmp')
933 rw_dst = os.path.join(rw_locale_dir, name + '.bmp')
934 shutil.copyfile(ro_src, rw_dst)
935
936 def create_locale_list(self):
937 """Creates locale list as a CSV file.
938
939 Each line in the file is of format "code,rtl", where
940 - "code": language code of the locale
941 - "rtl": "1" for right-to-left language, "0" otherwise
942 """
943 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
944 for locale_info in self.locales:
Yu-Ping Wu986dd8a2021-04-28 16:54:33 +0800945 f.write(f'{locale_info.code},{locale_info.rtl:d}\n')
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800946
947 def build(self):
948 """Builds all images required by a board."""
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800949 # Clean up output/stage directories
950 for path in (self.output_dir, self.stage_dir):
951 if os.path.exists(path):
952 shutil.rmtree(path)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800953 os.makedirs(self.output_dir)
Yu-Ping Wuf64557d2021-04-20 13:56:42 +0800954 os.makedirs(self.stage_dir)
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800955
956 print('Converting sprite images...')
957 self.convert_sprite_images()
958
959 print('Building generic strings...')
960 self.build_generic_strings()
961
962 print('Building localized strings...')
963 self.build_localized_strings()
964
965 print('Moving language images to locale-independent directory...')
966 self.move_language_images()
967
968 print('Creating locale list file...')
969 self.create_locale_list()
970
971 print('Building glyphs...')
972 self.build_glyphs()
973
974 print('Copying specified images to RW packing directory...')
975 self.copy_images_to_rw()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800976
977
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800978def main():
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800979 """Builds bitmaps for firmware screens."""
980 parser = argparse.ArgumentParser()
981 parser.add_argument('board', help='Target board')
982 args = parser.parse_args()
983 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800984
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800985 with open(FORMAT_FILE, encoding='utf-8') as f:
986 formats = yaml.load(f)
987 board_config = load_board_config(BOARDS_CONFIG_FILE, board)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800988
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800989 print('Building for ' + board)
990 check_fonts(formats[KEY_FONTS])
991 print('Output dir: ' + OUTPUT_DIR)
992 converter = Converter(board, formats, board_config, OUTPUT_DIR)
993 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800994
995
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800996if __name__ == '__main__':
Yu-Ping Wud6a7abb2021-04-14 15:36:31 +0800997 main()