blob: 972565d34904c8b2b37bdb7cdd39bafc59d13192 [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
Hung-Te Lin04addcc2015-03-23 18:43:30 +080013import multiprocessing
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080014import os
15import re
Jes Klinke1687a992020-06-16 13:47:17 -070016import shutil
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080017import signal
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080018import subprocess
Jes Klinke1687a992020-06-16 13:47:17 -070019import tempfile
Hung-Te Lin04addcc2015-03-23 18:43:30 +080020
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080021import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080022from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080023
24SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080025
26STRINGS_GRD_FILE = 'firmware_strings.grd'
27STRINGS_JSON_FILE_TMPL = '{}.json'
28FORMAT_FILE = 'format.yaml'
29BOARDS_CONFIG_FILE = 'boards.yaml'
30
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080031OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080032
33ONE_LINE_DIR = 'one_line'
34SVG_FILES = '*.svg'
35PNG_FILES = '*.png'
36
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080037DIAGNOSTIC_UI = os.getenv('DIAGNOSTIC_UI') == '1'
38
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080039# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080040KEY_DEFAULT = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080041KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080042KEY_GENERIC_FILES = 'generic_files'
43KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080044KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
45KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080046KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080047KEY_BGCOLOR = 'bgcolor'
48KEY_FGCOLOR = 'fgcolor'
49KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080050KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080051KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080052
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080053# Board config YAML key names.
Yu-Ping Wu60b45372021-03-31 16:56:08 +080054KEY_SCREEN = 'screen'
55KEY_PANEL = 'panel'
56KEY_SDCARD = 'sdcard'
57KEY_DPI = 'dpi'
58KEY_RTL = 'rtl'
59KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080060
61BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080062
Jes Klinke1687a992020-06-16 13:47:17 -070063# Regular expressions used to eliminate spurious spaces and newlines in
64# translation strings.
65NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
66NEWLINE_REPLACEMENT = r'\1 \2'
67CRLF_PATTERN = re.compile(r'\r\n')
68MULTIBLANK_PATTERN = re.compile(r' *')
69
Yu-Ping Wu3d07a062021-01-26 18:10:32 +080070# The base for bitmap scales, same as UI_SCALE in depthcharge. For example, if
71# `SCALE_BASE` is 1000, then height = 200 means 20% of the screen height. Also
72# see the 'styles' section in format.yaml.
73SCALE_BASE = 1000
74DEFAULT_GLYPH_HEIGHT = 20
75
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +080076GLYPH_FONT = 'Cousine'
Yu-Ping Wu11027f02020-10-14 17:35:42 +080077
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080078LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080079
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080080
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080081class DataError(Exception):
82 pass
83
84
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080085class BuildImageError(Exception):
86 """The exception class for all errors generated during build image process."""
87
88
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080089def get_config_with_defaults(configs, key):
90 """Gets config of `key` from `configs`.
91
92 If `key` is not present in `configs`, the default config will be returned.
93 Similarly, if some config values are missing for `key`, the default ones will
94 be used.
95 """
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080096 config = configs[KEY_DEFAULT].copy()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080097 config.update(configs.get(key, {}))
98 return config
99
100
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800101def load_boards_config(filename):
102 """Loads the configuration of all boards from `filename`.
103
104 Args:
105 filename: File name of a YAML config file.
106
107 Returns:
108 A dictionary mapping each board name to its config.
109 """
110 with open(filename, 'rb') as file:
111 raw = yaml.load(file)
112
113 configs = {}
114 default = raw[KEY_DEFAULT]
115 if not default:
116 raise BuildImageError('Default configuration is not found')
117 for boards, params in raw.items():
118 if boards == KEY_DEFAULT:
119 continue
120 config = copy.deepcopy(default)
121 if params:
122 config.update(params)
123 for board in boards.replace(',', ' ').split():
124 configs[board] = config
125
126 return configs
127
128
129def check_fonts(fonts):
130 """Check if all fonts are available."""
131 for locale, font in fonts.items():
132 if subprocess.run(['fc-list', '-q', font]).returncode != 0:
133 raise BuildImageError('Font %r not found for locale %r'
134 % (font, locale))
135
136
Yu-Ping Wu97046932021-01-25 17:38:56 +0800137def run_pango_view(input_file, output_file, locale, font, height, max_width,
138 dpi, bgcolor, fgcolor, hinting='full'):
139 """Run pango-view."""
140 command = ['pango-view', '-q']
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800141 if locale:
Yu-Ping Wu97046932021-01-25 17:38:56 +0800142 command += ['--language', locale]
143
144 # Font size should be proportional to the height. Here we use 2 as the
145 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
146 # will be roughly equivalent to setting the screen resolution to 1366x768.
147 font_size = height / 2
148 font_spec = '%s %r' % (font, font_size)
149 command += ['--font', font_spec]
150
Yu-Ping Wued95df32020-11-04 17:08:15 +0800151 if max_width:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800152 # When converting text to PNG by pango-view, the ratio of image height to
153 # the font size is usually no more than 1.1875 (with Roboto). Therefore,
154 # set the `max_width_pt` as follows to prevent UI drawing from exceeding
155 # the canvas boundary in depthcharge runtime. The divisor 2 is the same in
156 # the calculation of `font_size` above.
157 max_width_pt = int(max_width / 2 * 1.1875)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800158 command.append('--width=%d' % max_width_pt)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800159 if dpi:
160 command.append('--dpi=%d' % dpi)
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800161 command.append('--margin=0')
Yu-Ping Wu97046932021-01-25 17:38:56 +0800162 command += ['--background', bgcolor]
163 command += ['--foreground', fgcolor]
164 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800165
Yu-Ping Wu97046932021-01-25 17:38:56 +0800166 command += ['--output', output_file]
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800167 command.append(input_file)
168
Yu-Ping Wu97046932021-01-25 17:38:56 +0800169 subprocess.check_call(command, stdout=subprocess.PIPE)
170
171
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800172def parse_locale_json_file(locale, json_dir):
173 """Parses given firmware string json file.
174
175 Args:
176 locale: The name of the locale, e.g. "da" or "pt-BR".
177 json_dir: Directory containing json output from grit.
178
179 Returns:
180 A dictionary for mapping of "name to content" for files to be generated.
181 """
Jes Klinke1687a992020-06-16 13:47:17 -0700182 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800183 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800184 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700185 for tag, msgdict in json.load(input_file).items():
186 msgtext = msgdict['message']
187 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
188 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
189 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
190 # Strip any trailing whitespace. A trailing newline appears to make
191 # Pango report a larger layout size than what's actually visible.
192 msgtext = msgtext.strip()
193 result[tag] = msgtext
194 return result
195
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800196
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800197class Converter(object):
Yu-Ping Wu20913672021-03-24 15:25:10 +0800198 """Converter for converting sprites, texts, and glyphs to bitmaps.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800199
200 Attributes:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800201 DEFAULT_OUTPUT_EXT (str): Default output file extension.
Yu-Ping Wu20913672021-03-24 15:25:10 +0800202 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image sprites
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800203 to bitmaps.
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800204 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800205 DEFAULT_BACKGROUND (tuple): Default background color.
206 BACKGROUND_COLORS (dict): Background color of each image. Key is the image
207 name and value is a tuple of RGB values.
208 """
209
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800210 DEFAULT_OUTPUT_EXT = '.bmp'
211
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800212 # background colors
213 DEFAULT_BACKGROUND = (0x20, 0x21, 0x24)
214 LANG_HEADER_BACKGROUND = (0x16, 0x17, 0x19)
215 LINK_SELECTED_BACKGROUND = (0x2a, 0x2f, 0x39)
Yu-Ping Wu20913672021-03-24 15:25:10 +0800216 SPRITE_MAX_COLORS = 128
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800217 GLYPH_MAX_COLORS = 7
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800218
219 BACKGROUND_COLORS = {
220 'ic_dropdown': LANG_HEADER_BACKGROUND,
221 'ic_dropleft_focus': LINK_SELECTED_BACKGROUND,
222 'ic_dropright_focus': LINK_SELECTED_BACKGROUND,
223 'ic_globe': LANG_HEADER_BACKGROUND,
224 'ic_search_focus': LINK_SELECTED_BACKGROUND,
225 'ic_settings_focus': LINK_SELECTED_BACKGROUND,
226 'ic_power_focus': LINK_SELECTED_BACKGROUND,
227 }
228
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800229 def __init__(self, board, formats, board_config, output):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800230 """Inits converter.
231
232 Args:
233 board: Board name.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800234 formats: A dictionary of string formats.
235 board_config: A dictionary of board configurations.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800236 output: Output directory.
237 """
238 self.board = board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800239 self.formats = formats
240 self.config = board_config
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800241 self.set_dirs(output)
242 self.set_screen()
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800243 self.set_rename_map()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800244 self.set_locales()
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800245 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800246
247 def set_dirs(self, output):
248 """Sets board output directory and stage directory.
249
250 Args:
251 output: Output directory.
252 """
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800253 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
Yu-Ping Wu20913672021-03-24 15:25:10 +0800254 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800255 self.locale_dir = os.path.join(self.strings_dir, 'locale')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800256 self.output_dir = os.path.join(output, self.board)
257 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
258 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
259 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800260 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800261 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800262 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
263
264 def set_screen(self):
265 """Sets screen width and height."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800266 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800267
Yu-Ping Wue445e042020-11-19 15:53:42 +0800268 self.panel_stretch = fractions.Fraction(1)
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800269 if self.config[KEY_PANEL]:
Yu-Ping Wue445e042020-11-19 15:53:42 +0800270 # Calculate `panel_stretch`. It's used to shrink images horizontally so
271 # that the resulting images will look proportional to the original image
272 # on the stretched display. If the display is not stretched, meaning the
273 # aspect ratio is same as the screen where images were rendered, no
274 # shrinking is performed.
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800275 panel_width, panel_height = self.config[KEY_PANEL]
Yu-Ping Wue445e042020-11-19 15:53:42 +0800276 self.panel_stretch = fractions.Fraction(self.screen_width * panel_height,
277 self.screen_height * panel_width)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800278
Yu-Ping Wue445e042020-11-19 15:53:42 +0800279 if self.panel_stretch > 1:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800280 raise BuildImageError('Panel aspect ratio (%f) is smaller than screen '
281 'aspect ratio (%f). It indicates screen will be '
282 'shrunk horizontally. It is currently unsupported.'
283 % (panel_width / panel_height,
284 self.screen_width / self.screen_height))
285
286 # Set up square drawing area
287 self.canvas_px = min(self.screen_width, self.screen_height)
288
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800289 def set_rename_map(self):
290 """Initializes a dict `self.rename_map` for image renaming.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800291
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800292 For each items in the dict, image `key` will be renamed to `value`.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800293 """
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800294 is_detachable = os.getenv('DETACHABLE') == '1'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800295 physical_presence = os.getenv('PHYSICAL_PRESENCE')
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800296 rename_map = {}
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800297
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800298 # Navigation instructions
299 if is_detachable:
300 rename_map.update({
301 'nav-button_power': 'nav-key_enter',
302 'nav-button_volume_up': 'nav-key_up',
303 'nav-button_volume_down': 'nav-key_down',
304 'navigate0_tablet': 'navigate0',
305 'navigate1_tablet': 'navigate1',
306 })
307 else:
308 rename_map.update({
309 'nav-button_power': None,
310 'nav-button_volume_up': None,
311 'nav-button_volume_down': None,
312 'navigate0_tablet': None,
313 'navigate1_tablet': None,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800314 })
315
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800316 # Physical presence confirmation
317 if physical_presence == 'recovery':
318 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
319 rename_map['rec_to_dev_desc1_power'] = None
320 elif physical_presence == 'power':
321 rename_map['rec_to_dev_desc1_phyrec'] = None
322 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
323 else:
324 rename_map['rec_to_dev_desc1_phyrec'] = None
325 rename_map['rec_to_dev_desc1_power'] = None
326 if physical_presence != 'keyboard':
327 raise BuildImageError('Invalid physical presence setting %s for board '
328 '%s' % (physical_presence, self.board))
329
330 # Broken screen
331 if physical_presence == 'recovery':
332 rename_map['broken_desc_phyrec'] = 'broken_desc'
333 rename_map['broken_desc_detach'] = None
334 elif is_detachable:
335 rename_map['broken_desc_phyrec'] = None
336 rename_map['broken_desc_detach'] = 'broken_desc'
337 else:
338 rename_map['broken_desc_phyrec'] = None
339 rename_map['broken_desc_detach'] = None
340
341 # SD card
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800342 if not self.config[KEY_SDCARD]:
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800343 rename_map.update({
344 'rec_sel_desc1_no_sd': 'rec_sel_desc1',
345 'rec_sel_desc1_no_phone_no_sd': 'rec_sel_desc1_no_phone',
346 'rec_disk_step1_desc0_no_sd': 'rec_disk_step1_desc0',
347 })
348 else:
349 rename_map.update({
350 'rec_sel_desc1_no_sd': None,
351 'rec_sel_desc1_no_phone_no_sd': None,
352 'rec_disk_step1_desc0_no_sd': None,
353 })
354
355 # Check for duplicate new names
356 new_names = list(new_name for new_name in rename_map.values() if new_name)
357 if len(set(new_names)) != len(new_names):
358 raise BuildImageError('Duplicate values found in rename_map')
359
360 # Map new_name to None to skip image generation for it
361 for new_name in new_names:
362 if new_name not in rename_map:
363 rename_map[new_name] = None
364
365 # Print mapping
366 print('Rename map:')
367 for name, new_name in sorted(rename_map.items()):
368 print(' %s => %s' % (name, new_name))
369
370 self.rename_map = rename_map
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800371
372 def set_locales(self):
373 """Sets a list of locales for which localized images are converted."""
374 # LOCALES environment variable can overwrite boards.yaml
375 env_locales = os.getenv('LOCALES')
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800376 rtl_locales = set(self.config[KEY_RTL])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800377 if env_locales:
378 locales = env_locales.split()
379 else:
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800380 locales = self.config[KEY_LOCALES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800381 # Check rtl_locales are contained in locales.
382 unknown_rtl_locales = rtl_locales - set(locales)
383 if unknown_rtl_locales:
384 raise BuildImageError('Unknown locales %s in %s' %
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800385 (list(unknown_rtl_locales), KEY_RTL))
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800386 self.locales = [LocaleInfo(code, code in rtl_locales)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800387 for code in locales]
388
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800389 @classmethod
390 def get_text_colors(cls, dpi):
391 """Derive maximum text colors from `dpi`."""
392 if dpi < 64:
393 return 2
394 elif dpi < 72:
395 return 3
396 elif dpi < 80:
397 return 4
398 elif dpi < 96:
399 return 5
400 elif dpi < 112:
401 return 6
402 else:
403 return 7
404
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800405 def _to_px(self, length, num_lines=1):
406 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu3d07a062021-01-26 18:10:32 +0800407 return int(self.canvas_px * length / SCALE_BASE) * num_lines
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800408
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800409 def _get_png_height(self, png_file):
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800410 # With small DPI, pango-view may generate an empty file
411 if os.path.getsize(png_file) == 0:
412 return 0
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800413 with Image.open(png_file) as image:
414 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800415
416 def get_num_lines(self, file, one_line_dir):
417 """Gets the number of lines of text in `file`."""
418 name, _ = os.path.splitext(os.path.basename(file))
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800419 png_name = name + '.png'
420 multi_line_file = os.path.join(os.path.dirname(file), png_name)
421 one_line_file = os.path.join(one_line_dir, png_name)
422 # The number of lines is determined by comparing the height of
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800423 # `multi_line_file` with `one_line_file`, where the latter is generated
424 # without the '--width' option passed to pango-view.
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800425 height = self._get_png_height(multi_line_file)
426 line_height = self._get_png_height(one_line_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800427 return int(round(height / line_height))
428
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800429 def convert_svg_to_png(self, svg_file, png_file, height, num_lines,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800430 background):
431 """Converts .svg file to .png file."""
432 background_hex = ''.join(format(x, '02x') for x in background)
433 # If the width/height of the SVG file is specified in points, the
434 # rsvg-convert command with default 90DPI will potentially cause the pixels
435 # at the right/bottom border of the output image to be transparent (or
436 # filled with the specified background color). This seems like an
437 # rsvg-convert issue regarding image scaling. Therefore, use 72DPI here
438 # to avoid the scaling.
439 command = ['rsvg-convert',
440 '--background-color', "'#%s'" % background_hex,
441 '--dpi-x', '72',
442 '--dpi-y', '72',
443 '-o', png_file]
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800444 height_px = self._to_px(height, num_lines)
Yu-Ping Wue445e042020-11-19 15:53:42 +0800445 if height_px <= 0:
446 raise BuildImageError('Height of %r <= 0 (%dpx)' %
447 (os.path.basename(svg_file), height_px))
448 command.extend(['--height', '%d' % height_px])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800449 command.append(svg_file)
450 subprocess.check_call(' '.join(command), shell=True)
451
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800452 def convert_to_bitmap(self, input_file, num_lines, background, output,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800453 max_colors):
454 """Converts an image file `input_file` to a BMP file `output`."""
455 image = Image.open(input_file)
456
457 # Process alpha channel and transparency.
458 if image.mode == 'RGBA':
459 target = Image.new('RGB', image.size, background)
460 image.load() # required for image.split()
461 mask = image.split()[-1]
462 target.paste(image, mask=mask)
463 elif (image.mode == 'P') and ('transparency' in image.info):
464 exit('Sorry, PNG with RGBA palette is not supported.')
465 elif image.mode != 'RGB':
466 target = image.convert('RGB')
467 else:
468 target = image
469
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800470 width_px, height_px = image.size
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800471 # Stretch image horizontally for stretched display.
Yu-Ping Wue445e042020-11-19 15:53:42 +0800472 if self.panel_stretch != 1:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800473 width_px = int(width_px * self.panel_stretch)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800474 target = target.resize((width_px, height_px), Image.BICUBIC)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800475
476 # Export and downsample color space.
477 target.convert('P', dither=None, colors=max_colors, palette=Image.ADAPTIVE
478 ).save(output)
479
480 with open(output, 'rb+') as f:
481 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
482 f.write(bytearray([num_lines]))
483
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800484 def convert(self, file, output, height, max_width, max_colors,
Yu-Ping Wued95df32020-11-04 17:08:15 +0800485 one_line_dir=None):
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800486 """Converts image `file` to bitmap format."""
487 name, ext = os.path.splitext(os.path.basename(file))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800488
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800489 background = self.BACKGROUND_COLORS.get(name, self.DEFAULT_BACKGROUND)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800490
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800491 # Determine num_lines in order to scale the image
492 if one_line_dir and max_width:
493 num_lines = self.get_num_lines(file, one_line_dir)
494 else:
495 num_lines = 1
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800496
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800497 if ext == '.svg':
498 png_file = os.path.join(self.temp_dir, name + '.png')
499 self.convert_svg_to_png(file, png_file, height, num_lines, background)
500 file = png_file
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800501
Yu-Ping Wub87a47d2021-03-30 14:10:22 +0800502 self.convert_to_bitmap(file, num_lines, background, output, max_colors)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800503
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800504 def _bisect_dpi(self, max_dpi, initial_dpi, max_height_px, get_height):
505 """Bisects to find the DPI that produces image height `max_height_px`.
506
507 Args:
508 max_dpi: Maximum DPI for binary search.
509 initial_dpi: Initial DPI to try with in binary search.
510 If specified, the value must be no larger than `max_dpi`.
511 max_height_px: Maximum (target) height to search for.
512 get_height: A function converting DPI to height. The function is called
513 once before returning.
514
515 Returns:
516 The best integer DPI within [1, `max_dpi`].
517 """
518
519 min_dpi = 1
520 first_iter = True
521
522 min_height_px = get_height(min_dpi)
523 if min_height_px > max_height_px:
524 # For some font such as "Noto Sans CJK SC", the generated height cannot
525 # go below a certain value. In this case, find max DPI with
526 # height_px <= min_height_px.
527 while min_dpi < max_dpi:
528 if first_iter and initial_dpi:
529 mid_dpi = initial_dpi
530 else:
531 mid_dpi = (min_dpi + max_dpi + 1) // 2
532 height_px = get_height(mid_dpi)
533 if height_px > min_height_px:
534 max_dpi = mid_dpi - 1
535 else:
536 min_dpi = mid_dpi
537 first_iter = False
538 get_height(max_dpi)
539 return max_dpi
540
541 # Find min DPI with height_px == max_height_px
542 while min_dpi < max_dpi:
543 if first_iter and initial_dpi:
544 mid_dpi = initial_dpi
545 else:
546 mid_dpi = (min_dpi + max_dpi) // 2
547 height_px = get_height(mid_dpi)
548 if height_px == max_height_px:
549 return mid_dpi
550 elif height_px < max_height_px:
551 min_dpi = mid_dpi + 1
552 else:
553 max_dpi = mid_dpi
554 first_iter = False
555 get_height(min_dpi)
556 return min_dpi
557
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800558 def convert_text_to_image(self, locale, input_file, output_file, font,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800559 stage_dir, max_colors, height=None, max_width=None,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800560 dpi=None, initial_dpi=None,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800561 bgcolor='#000000', fgcolor='#ffffff',
562 use_svg=False):
563 """Converts text file `input_file` into image file.
564
565 Because pango-view does not support assigning output format options for
566 bitmap, we must create images in SVG/PNG format and then post-process them
567 (e.g. convert into BMP by ImageMagick).
568
569 Args:
570 locale: Locale (language) to select implicit rendering options. None for
571 locale-independent strings.
572 input_file: Path of input text file.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800573 output_file: Path of output image file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800574 font: Font name.
575 stage_dir: Directory to store intermediate file(s).
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800576 max_colors: Maximum colors to convert to bitmap.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800577 height: Image height relative to the screen resolution.
578 max_width: Maximum image width relative to the screen resolution.
579 dpi: DPI value passed to pango-view.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800580 initial_dpi: Initial DPI to try with in binary search.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800581 bgcolor: Background color (#rrggbb).
582 fgcolor: Foreground color (#rrggbb).
583 use_svg: If set to True, generate SVG file. Otherwise, generate PNG file.
584
585 Returns:
586 Effective DPI, or `None` when not applicable.
587 """
588 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
589 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800590
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800591 name, _ = os.path.splitext(os.path.basename(input_file))
592 svg_file = os.path.join(stage_dir, name + '.svg')
593 png_file = os.path.join(stage_dir, name + '.png')
594 png_file_one_line = os.path.join(one_line_dir, name + '.png')
595
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800596 def get_one_line_png_height(dpi):
597 """Generates a one-line PNG using DPI `dpi` and returns its height."""
598 run_pango_view(input_file, png_file_one_line, locale, font, height, 0,
599 dpi, bgcolor, fgcolor)
600 return self._get_png_height(png_file_one_line)
601
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800602 if use_svg:
603 run_pango_view(input_file, svg_file, locale, font, height, 0, dpi,
604 bgcolor, fgcolor, hinting='none')
Yu-Ping Wub87a47d2021-03-30 14:10:22 +0800605 self.convert(svg_file, output_file, height, max_width, max_colors)
606 return None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800607 else:
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800608 if not dpi:
609 raise BuildImageError('DPI must be specified with use_svg=False')
610
611 eff_dpi = dpi
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800612 if locale:
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800613 max_height_px = self._to_px(height)
614 height_px = get_one_line_png_height(dpi)
615 if height_px > max_height_px:
616 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
617 get_one_line_png_height)
618 # NOTE: With the same DPI, the height of multi-line PNG is not necessarily
619 # a multiple of the height of one-line PNG. Therefore, even with the
620 # binary search, the height of the resulting multi-line PNG might be
621 # less than "one_line_height * num_lines". We cannot binary-search DPI
622 # for multi-line PNGs because "num_lines" is dependent on DPI.
623 run_pango_view(input_file, png_file, locale, font, height, max_width,
624 eff_dpi, bgcolor, fgcolor)
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800625 self.convert(png_file, output_file, height, max_width, max_colors,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800626 one_line_dir=one_line_dir if locale else None)
627 return eff_dpi
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800628
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800629 def convert_sprite_images(self):
630 """Converts sprite images."""
631 names = self.formats[KEY_SPRITE_FILES]
632 styles = self.formats[KEY_STYLES]
633 # Check redundant images
Yu-Ping Wu20913672021-03-24 15:25:10 +0800634 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800635 name, _ = os.path.splitext(os.path.basename(filename))
636 if name not in names:
637 raise BuildImageError('Sprite image %r not specified in %s' %
638 (filename, FORMAT_FILE))
639 # Convert images
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800640 for name, category in names.items():
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800641 new_name = self.rename_map.get(name, name)
642 if not new_name:
643 continue
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800644 style = get_config_with_defaults(styles, category)
Yu-Ping Wu20913672021-03-24 15:25:10 +0800645 file = os.path.join(self.sprite_dir, name + '.svg')
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800646 output = os.path.join(self.output_dir, new_name + self.DEFAULT_OUTPUT_EXT)
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800647 height = style[KEY_HEIGHT]
Yu-Ping Wu20913672021-03-24 15:25:10 +0800648 self.convert(file, output, height, None, self.SPRITE_MAX_COLORS)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800649
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800650 def build_generic_strings(self):
651 """Builds images of generic (locale-independent) strings."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800652 dpi = self.config[KEY_DPI]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800653
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800654 names = self.formats[KEY_GENERIC_FILES]
655 styles = self.formats[KEY_STYLES]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800656 fonts = self.formats[KEY_FONTS]
657 default_font = fonts[KEY_DEFAULT]
658
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800659 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
660 name, _ = os.path.splitext(os.path.basename(txt_file))
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800661 new_name = self.rename_map.get(name, name)
662 if not new_name:
663 continue
664 output_file = os.path.join(self.output_dir,
665 new_name + self.DEFAULT_OUTPUT_EXT)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800666 category = names[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800667 style = get_config_with_defaults(styles, category)
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800668 self.convert_text_to_image(None, txt_file, output_file, default_font,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800669 self.stage_dir, self.text_max_colors,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800670 height=style[KEY_HEIGHT],
671 max_width=style[KEY_MAX_WIDTH],
672 dpi=dpi,
673 bgcolor=style[KEY_BGCOLOR],
674 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800675
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800676 def build_locale(self, locale, names, json_dir):
677 """Builds images of strings for `locale`."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800678 dpi = self.config[KEY_DPI]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800679 styles = self.formats[KEY_STYLES]
680 fonts = self.formats[KEY_FONTS]
681 font = fonts.get(locale, fonts[KEY_DEFAULT])
682 inputs = parse_locale_json_file(locale, json_dir)
683
684 # Walk locale directory to add pre-generated texts such as language names.
685 for txt_file in glob.glob(os.path.join(self.locale_dir, locale, '*.txt')):
686 name, _ = os.path.splitext(os.path.basename(txt_file))
687 with open(txt_file, 'r', encoding='utf-8-sig') as f:
688 inputs[name] = f.read().strip()
689
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800690 stage_dir = os.path.join(self.stage_locale_dir, locale)
691 os.makedirs(stage_dir, exist_ok=True)
692 output_dir = os.path.join(self.output_ro_dir, locale)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800693 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800694
695 eff_dpi_counters = defaultdict(Counter)
696 results = []
697 for name, category in sorted(names.items()):
698 # Ignore missing translation
699 if locale != 'en' and name not in inputs:
700 continue
701
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800702 new_name = self.rename_map.get(name, name)
703 if not new_name:
704 continue
705 output_file = os.path.join(output_dir, new_name + self.DEFAULT_OUTPUT_EXT)
706
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800707 # Write to text file
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800708 text_file = os.path.join(stage_dir, name + '.txt')
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800709 with open(text_file, 'w', encoding='utf-8-sig') as f:
710 f.write(inputs[name] + '\n')
711
712 # Convert text to image
713 style = get_config_with_defaults(styles, category)
714 height = style[KEY_HEIGHT]
715 eff_dpi_counter = eff_dpi_counters[height]
716 if eff_dpi_counter:
717 # Find the effective DPI that appears most times for `height`. This
718 # avoid doing the same binary search again and again. In case of a tie,
719 # pick the largest DPI.
720 best_eff_dpi = max(eff_dpi_counter,
721 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
722 else:
723 best_eff_dpi = None
724 eff_dpi = self.convert_text_to_image(locale,
725 text_file,
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800726 output_file,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800727 font,
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800728 stage_dir,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800729 self.text_max_colors,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800730 height=height,
731 max_width=style[KEY_MAX_WIDTH],
732 dpi=dpi,
733 initial_dpi=best_eff_dpi,
734 bgcolor=style[KEY_BGCOLOR],
735 fgcolor=style[KEY_FGCOLOR])
736 eff_dpi_counter[eff_dpi] += 1
737 assert eff_dpi <= dpi
738 if eff_dpi != dpi:
739 results.append(eff_dpi)
740 return results
741
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800742 def _check_text_width(self, names):
743 """Checks if text image will exceed the expected drawing area at runtime."""
744 styles = self.formats[KEY_STYLES]
745
746 for locale_info in self.locales:
747 locale = locale_info.code
748 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
749 for filename in glob.glob(os.path.join(ro_locale_dir,
750 '*' + self.DEFAULT_OUTPUT_EXT)):
751 name, _ = os.path.splitext(os.path.basename(filename))
752 category = names[name]
753 style = get_config_with_defaults(styles, category)
754 height = style[KEY_HEIGHT]
755 max_width = style[KEY_MAX_WIDTH]
756 if not max_width:
757 continue
758 max_width_px = self._to_px(max_width)
759 with open(filename, 'rb') as f:
760 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
761 num_lines = f.read(1)[0]
762 height_px = self._to_px(height * num_lines)
763 with Image.open(filename) as image:
764 width_px = height_px * image.size[0] // image.size[1]
765 if width_px > max_width_px:
766 raise BuildImageError('%s: Image width %dpx greater than max width '
767 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800768
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800769 def _copy_missing_bitmaps(self):
770 """Copy missing (not yet translated) strings from locale 'en'."""
771 en_files = glob.glob(os.path.join(self.output_ro_dir, 'en',
772 '*' + self.DEFAULT_OUTPUT_EXT))
773 for locale_info in self.locales:
774 locale = locale_info.code
775 if locale == 'en':
776 continue
777 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
778 for en_file in en_files:
779 filename = os.path.basename(en_file)
780 locale_file = os.path.join(ro_locale_dir, filename)
781 if not os.path.isfile(locale_file):
782 print("WARNING: Locale '%s': copying '%s'" % (locale, filename))
783 shutil.copyfile(en_file, locale_file)
784
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800785 def build_localized_strings(self):
786 """Builds images of localized strings."""
787 # Sources are one .grd file with identifiers chosen by engineers and
788 # corresponding English texts, as well as a set of .xtb files (one for each
789 # language other than US English) with a mapping from hash to translation.
790 # Because the keys in the .xtb files are a hash of the English source text,
791 # rather than our identifiers, such as "btn_cancel", we use the "grit"
792 # command line tool to process the .grd and .xtb files, producing a set of
793 # .json files mapping our identifier to the translated string, one for every
794 # language including US English.
795
796 # Create a temporary directory to place the translation output from grit in.
797 json_dir = tempfile.mkdtemp()
798
799 # This invokes the grit build command to generate JSON files from the XTB
800 # files containing translations. The results are placed in `json_dir` as
801 # specified in firmware_strings.grd, i.e. one JSON file per locale.
802 subprocess.check_call([
803 'grit',
804 '-i', os.path.join(self.locale_dir, STRINGS_GRD_FILE),
805 'build',
806 '-o', os.path.join(json_dir),
807 ])
808
809 # Make a copy to avoid modifying `self.formats`
810 names = copy.deepcopy(self.formats[KEY_LOCALIZED_FILES])
811 if DIAGNOSTIC_UI:
812 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
813
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800814 # Ignore SIGINT in child processes
815 sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
816 pool = multiprocessing.Pool(multiprocessing.cpu_count())
817 signal.signal(signal.SIGINT, sigint_handler)
818
819 results = []
820 for locale_info in self.locales:
821 locale = locale_info.code
822 print(locale, end=' ', flush=True)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800823 args = (
824 locale,
825 names,
826 json_dir,
827 )
828 results.append(pool.apply_async(self.build_locale, args))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800829
830 print()
831 pool.close()
832
833 try:
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800834 results = [r.get() for r in results]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800835 except KeyboardInterrupt:
836 pool.terminate()
837 pool.join()
838 exit('Aborted by user')
839 else:
840 pool.join()
841
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800842 effective_dpi = [dpi for r in results for dpi in r if dpi]
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800843 if effective_dpi:
844 print('Reducing effective DPI to %d, limited by screen resolution' %
845 max(effective_dpi))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800846
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800847 shutil.rmtree(json_dir)
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800848 self._check_text_width(names)
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800849 self._copy_missing_bitmaps()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800850
851 def move_language_images(self):
852 """Renames language bitmaps and move to self.output_dir.
853
854 The directory self.output_dir contains locale-independent images, and is
855 used for creating vbgfx.bin by archive_images.py.
856 """
857 for locale_info in self.locales:
858 locale = locale_info.code
859 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
860 old_file = os.path.join(ro_locale_dir, 'language.bmp')
861 new_file = os.path.join(self.output_dir, 'language_%s.bmp' % locale)
862 if os.path.exists(new_file):
863 raise BuildImageError('File already exists: %s' % new_file)
864 shutil.move(old_file, new_file)
865
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800866 def build_glyphs(self):
867 """Builds glyphs of ascii characters."""
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800868 os.makedirs(self.stage_glyph_dir, exist_ok=True)
869 output_dir = os.path.join(self.output_dir, 'glyph')
870 os.makedirs(output_dir)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800871 # TODO(b/163109632): Parallelize the conversion of glyphs
872 for c in range(ord(' '), ord('~') + 1):
873 name = f'idx{c:03d}_{c:02x}'
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800874 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800875 with open(txt_file, 'w', encoding='ascii') as f:
876 f.write(chr(c))
877 f.write('\n')
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800878 output_file = os.path.join(output_dir, name + self.DEFAULT_OUTPUT_EXT)
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800879 self.convert_text_to_image(None, txt_file, output_file, GLYPH_FONT,
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800880 self.stage_glyph_dir, self.GLYPH_MAX_COLORS,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800881 height=DEFAULT_GLYPH_HEIGHT,
882 use_svg=True)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800883
884 def copy_images_to_rw(self):
885 """Copies localized images specified in boards.yaml for RW override."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800886 if not self.config[KEY_RW_OVERRIDE]:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800887 print(' No localized images are specified for RW, skipping')
888 return
889
890 for locale_info in self.locales:
891 locale = locale_info.code
Chung-Sheng Wucd3b4e22021-04-01 18:50:20 +0800892 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
893 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800894 os.makedirs(rw_locale_dir)
895
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800896 for name in self.config[KEY_RW_OVERRIDE]:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800897 ro_src = os.path.join(ro_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
898 rw_dst = os.path.join(rw_locale_dir, name + self.DEFAULT_OUTPUT_EXT)
899 shutil.copyfile(ro_src, rw_dst)
900
901 def create_locale_list(self):
902 """Creates locale list as a CSV file.
903
904 Each line in the file is of format "code,rtl", where
905 - "code": language code of the locale
906 - "rtl": "1" for right-to-left language, "0" otherwise
907 """
908 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
909 for locale_info in self.locales:
910 f.write('{},{}\n'.format(locale_info.code,
911 int(locale_info.rtl)))
912
913 def build(self):
914 """Builds all images required by a board."""
915 # Clean up output directory
916 if os.path.exists(self.output_dir):
917 shutil.rmtree(self.output_dir)
918 os.makedirs(self.output_dir)
919
920 if not os.path.exists(self.stage_dir):
921 raise BuildImageError('Missing stage folder. Run make in strings dir.')
922
923 # Clean up temp directory
924 if os.path.exists(self.temp_dir):
925 shutil.rmtree(self.temp_dir)
926 os.makedirs(self.temp_dir)
927
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800928 print('Converting sprite images...')
929 self.convert_sprite_images()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800930
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800931 print('Building generic strings...')
932 self.build_generic_strings()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800933
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800934 print('Building localized strings...')
935 self.build_localized_strings()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800936
937 print('Moving language images to locale-independent directory...')
938 self.move_language_images()
939
940 print('Creating locale list file...')
941 self.create_locale_list()
942
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800943 print('Building glyphs...')
944 self.build_glyphs()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800945
946 print('Copying specified images to RW packing directory...')
947 self.copy_images_to_rw()
948
949
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800950def main():
951 """Builds bitmaps for firmware screens."""
952 parser = argparse.ArgumentParser()
953 parser.add_argument('board', help='Target board')
954 args = parser.parse_args()
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800955 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800956
957 with open(FORMAT_FILE, encoding='utf-8') as f:
958 formats = yaml.load(f)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800959 board_config = load_boards_config(BOARDS_CONFIG_FILE)[board]
960
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800961 print('Building for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800962 check_fonts(formats[KEY_FONTS])
963 print('Output dir: ' + OUTPUT_DIR)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800964 converter = Converter(board, formats, board_config, OUTPUT_DIR)
965 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800966
967
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800968if __name__ == '__main__':
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800969 main()