blob: 7b8cc6687780f529c9151248c1b6ba81e22c7aba [file] [log] [blame]
Yu-Ping Wud71b4452020-06-16 11:00:26 +08001#!/usr/bin/env python
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08005"""Script to generate bitmaps for firmware screens."""
Hung-Te Lin707e2ef2013-08-06 10:20:04 +08006
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08007import argparse
Yu-Ping Wu49606eb2021-03-03 22:43:19 +08008from collections import defaultdict, namedtuple, Counter
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +08009import copy
Yu-Ping Wue445e042020-11-19 15:53:42 +080010import fractions
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080011import glob
Jes Klinke1687a992020-06-16 13:47:17 -070012import json
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +080013from concurrent.futures import ProcessPoolExecutor
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080014import os
15import re
Jes Klinke1687a992020-06-16 13:47:17 -070016import shutil
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080017import subprocess
Jes Klinke1687a992020-06-16 13:47:17 -070018import tempfile
Hung-Te Lin04addcc2015-03-23 18:43:30 +080019
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080020import yaml
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080021from PIL import Image
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080022
23SCRIPT_BASE = os.path.dirname(os.path.abspath(__file__))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080024
25STRINGS_GRD_FILE = 'firmware_strings.grd'
26STRINGS_JSON_FILE_TMPL = '{}.json'
27FORMAT_FILE = 'format.yaml'
28BOARDS_CONFIG_FILE = 'boards.yaml'
29
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080030OUTPUT_DIR = os.getenv('OUTPUT', os.path.join(SCRIPT_BASE, 'build'))
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080031
32ONE_LINE_DIR = 'one_line'
33SVG_FILES = '*.svg'
34PNG_FILES = '*.png'
35
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080036DIAGNOSTIC_UI = os.getenv('DIAGNOSTIC_UI') == '1'
37
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080038# String format YAML key names.
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080039KEY_DEFAULT = '_DEFAULT_'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080040KEY_LOCALES = 'locales'
Yu-Ping Wu338f0832020-10-23 16:14:40 +080041KEY_GENERIC_FILES = 'generic_files'
42KEY_LOCALIZED_FILES = 'localized_files'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080043KEY_DIAGNOSTIC_FILES = 'diagnostic_files'
44KEY_SPRITE_FILES = 'sprite_files'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080045KEY_STYLES = 'styles'
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080046KEY_BGCOLOR = 'bgcolor'
47KEY_FGCOLOR = 'fgcolor'
48KEY_HEIGHT = 'height'
Yu-Ping Wued95df32020-11-04 17:08:15 +080049KEY_MAX_WIDTH = 'max_width'
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080050KEY_FONTS = 'fonts'
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080051
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080052# Board config YAML key names.
Yu-Ping Wu60b45372021-03-31 16:56:08 +080053KEY_SCREEN = 'screen'
54KEY_PANEL = 'panel'
55KEY_SDCARD = 'sdcard'
56KEY_DPI = 'dpi'
57KEY_RTL = 'rtl'
58KEY_RW_OVERRIDE = 'rw_override'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080059
60BMP_HEADER_OFFSET_NUM_LINES = 6
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080061
Jes Klinke1687a992020-06-16 13:47:17 -070062# Regular expressions used to eliminate spurious spaces and newlines in
63# translation strings.
64NEWLINE_PATTERN = re.compile(r'([^\n])\n([^\n])')
65NEWLINE_REPLACEMENT = r'\1 \2'
66CRLF_PATTERN = re.compile(r'\r\n')
67MULTIBLANK_PATTERN = re.compile(r' *')
68
Yu-Ping Wu3d07a062021-01-26 18:10:32 +080069# The base for bitmap scales, same as UI_SCALE in depthcharge. For example, if
70# `SCALE_BASE` is 1000, then height = 200 means 20% of the screen height. Also
71# see the 'styles' section in format.yaml.
72SCALE_BASE = 1000
73DEFAULT_GLYPH_HEIGHT = 20
74
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +080075GLYPH_FONT = 'Cousine'
Yu-Ping Wu11027f02020-10-14 17:35:42 +080076
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +080077LocaleInfo = namedtuple('LocaleInfo', ['code', 'rtl'])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080078
Yu-Ping Wu6b282c52020-03-19 12:54:15 +080079
Hung-Te Lin707e2ef2013-08-06 10:20:04 +080080class DataError(Exception):
81 pass
82
83
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +080084class BuildImageError(Exception):
85 """The exception class for all errors generated during build image process."""
86
87
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080088def get_config_with_defaults(configs, key):
89 """Gets config of `key` from `configs`.
90
91 If `key` is not present in `configs`, the default config will be returned.
92 Similarly, if some config values are missing for `key`, the default ones will
93 be used.
94 """
Yu-Ping Wu177f12c2020-11-04 15:55:37 +080095 config = configs[KEY_DEFAULT].copy()
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +080096 config.update(configs.get(key, {}))
97 return config
98
99
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800100def load_board_config(filename, board):
101 """Loads the configuration of `board` from `filename`.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800102
103 Args:
104 filename: File name of a YAML config file.
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800105 board: Board name.
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800106
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
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800113 config = copy.deepcopy(raw[KEY_DEFAULT])
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800114 for boards, params in raw.items():
115 if boards == KEY_DEFAULT:
116 continue
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800117 if board not in boards.split(','):
118 continue
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800119 if params:
120 config.update(params)
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800121 break
122 else:
123 raise BuildImageError('Board config not found for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800124
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800125 return config
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800126
127
128def check_fonts(fonts):
129 """Check if all fonts are available."""
130 for locale, font in fonts.items():
131 if subprocess.run(['fc-list', '-q', font]).returncode != 0:
132 raise BuildImageError('Font %r not found for locale %r'
133 % (font, locale))
134
135
Yu-Ping Wu97046932021-01-25 17:38:56 +0800136def run_pango_view(input_file, output_file, locale, font, height, max_width,
137 dpi, bgcolor, fgcolor, hinting='full'):
138 """Run pango-view."""
139 command = ['pango-view', '-q']
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800140 if locale:
Yu-Ping Wu97046932021-01-25 17:38:56 +0800141 command += ['--language', locale]
142
143 # Font size should be proportional to the height. Here we use 2 as the
144 # divisor so that setting dpi to 96 (pango-view's default) in boards.yaml
145 # will be roughly equivalent to setting the screen resolution to 1366x768.
146 font_size = height / 2
147 font_spec = '%s %r' % (font, font_size)
148 command += ['--font', font_spec]
149
Yu-Ping Wued95df32020-11-04 17:08:15 +0800150 if max_width:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800151 # When converting text to PNG by pango-view, the ratio of image height to
152 # the font size is usually no more than 1.1875 (with Roboto). Therefore,
153 # set the `max_width_pt` as follows to prevent UI drawing from exceeding
154 # the canvas boundary in depthcharge runtime. The divisor 2 is the same in
155 # the calculation of `font_size` above.
156 max_width_pt = int(max_width / 2 * 1.1875)
Yu-Ping Wued95df32020-11-04 17:08:15 +0800157 command.append('--width=%d' % max_width_pt)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800158 if dpi:
159 command.append('--dpi=%d' % dpi)
Yu-Ping Wucc86d6a2020-11-27 12:48:19 +0800160 command.append('--margin=0')
Yu-Ping Wu97046932021-01-25 17:38:56 +0800161 command += ['--background', bgcolor]
162 command += ['--foreground', fgcolor]
163 command += ['--hinting', hinting]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800164
Yu-Ping Wu97046932021-01-25 17:38:56 +0800165 command += ['--output', output_file]
Yu-Ping Wu11027f02020-10-14 17:35:42 +0800166 command.append(input_file)
167
Yu-Ping Wu97046932021-01-25 17:38:56 +0800168 subprocess.check_call(command, stdout=subprocess.PIPE)
169
170
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800171def parse_locale_json_file(locale, json_dir):
172 """Parses given firmware string json file.
173
174 Args:
175 locale: The name of the locale, e.g. "da" or "pt-BR".
176 json_dir: Directory containing json output from grit.
177
178 Returns:
179 A dictionary for mapping of "name to content" for files to be generated.
180 """
Jes Klinke1687a992020-06-16 13:47:17 -0700181 result = {}
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800182 filename = os.path.join(json_dir, STRINGS_JSON_FILE_TMPL.format(locale))
Yu-Ping Wud71b4452020-06-16 11:00:26 +0800183 with open(filename, encoding='utf-8-sig') as input_file:
Jes Klinke1687a992020-06-16 13:47:17 -0700184 for tag, msgdict in json.load(input_file).items():
185 msgtext = msgdict['message']
186 msgtext = re.sub(CRLF_PATTERN, '\n', msgtext)
187 msgtext = re.sub(NEWLINE_PATTERN, NEWLINE_REPLACEMENT, msgtext)
188 msgtext = re.sub(MULTIBLANK_PATTERN, ' ', msgtext)
189 # Strip any trailing whitespace. A trailing newline appears to make
190 # Pango report a larger layout size than what's actually visible.
191 msgtext = msgtext.strip()
192 result[tag] = msgtext
193 return result
194
Yu-Ping Wuae79af62020-09-23 16:48:06 +0800195
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800196class Converter(object):
Yu-Ping Wu20913672021-03-24 15:25:10 +0800197 """Converter for converting sprites, texts, and glyphs to bitmaps.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800198
199 Attributes:
Yu-Ping Wu20913672021-03-24 15:25:10 +0800200 SPRITE_MAX_COLORS (int): Maximum colors to use for converting image sprites
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800201 to bitmaps.
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800202 GLYPH_MAX_COLORS (int): Maximum colors to use for glyph bitmaps.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800203 """
204
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800205 # Max colors
Yu-Ping Wu20913672021-03-24 15:25:10 +0800206 SPRITE_MAX_COLORS = 128
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800207 GLYPH_MAX_COLORS = 7
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800208
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800209 def __init__(self, board, formats, board_config, output):
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800210 """Inits converter.
211
212 Args:
213 board: Board name.
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800214 formats: A dictionary of string formats.
215 board_config: A dictionary of board configurations.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800216 output: Output directory.
217 """
218 self.board = board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800219 self.formats = formats
220 self.config = board_config
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800221 self.set_dirs(output)
222 self.set_screen()
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800223 self.set_rename_map()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800224 self.set_locales()
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800225 self.text_max_colors = self.get_text_colors(self.config[KEY_DPI])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800226
227 def set_dirs(self, output):
228 """Sets board output directory and stage directory.
229
230 Args:
231 output: Output directory.
232 """
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800233 self.strings_dir = os.path.join(SCRIPT_BASE, 'strings')
Yu-Ping Wu20913672021-03-24 15:25:10 +0800234 self.sprite_dir = os.path.join(SCRIPT_BASE, 'sprite')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800235 self.locale_dir = os.path.join(self.strings_dir, 'locale')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800236 self.output_dir = os.path.join(output, self.board)
237 self.output_ro_dir = os.path.join(self.output_dir, 'locale', 'ro')
238 self.output_rw_dir = os.path.join(self.output_dir, 'locale', 'rw')
239 self.stage_dir = os.path.join(output, '.stage')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800240 self.stage_locale_dir = os.path.join(self.stage_dir, 'locale')
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800241 self.stage_glyph_dir = os.path.join(self.stage_dir, 'glyph')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800242 self.temp_dir = os.path.join(self.stage_dir, 'tmp')
243
244 def set_screen(self):
245 """Sets screen width and height."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800246 self.screen_width, self.screen_height = self.config[KEY_SCREEN]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800247
Yu-Ping Wue445e042020-11-19 15:53:42 +0800248 self.panel_stretch = fractions.Fraction(1)
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800249 if self.config[KEY_PANEL]:
Yu-Ping Wue445e042020-11-19 15:53:42 +0800250 # Calculate `panel_stretch`. It's used to shrink images horizontally so
251 # that the resulting images will look proportional to the original image
252 # on the stretched display. If the display is not stretched, meaning the
253 # aspect ratio is same as the screen where images were rendered, no
254 # shrinking is performed.
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800255 panel_width, panel_height = self.config[KEY_PANEL]
Yu-Ping Wue445e042020-11-19 15:53:42 +0800256 self.panel_stretch = fractions.Fraction(self.screen_width * panel_height,
257 self.screen_height * panel_width)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800258
Yu-Ping Wue445e042020-11-19 15:53:42 +0800259 if self.panel_stretch > 1:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800260 raise BuildImageError('Panel aspect ratio (%f) is smaller than screen '
261 'aspect ratio (%f). It indicates screen will be '
262 'shrunk horizontally. It is currently unsupported.'
263 % (panel_width / panel_height,
264 self.screen_width / self.screen_height))
265
266 # Set up square drawing area
267 self.canvas_px = min(self.screen_width, self.screen_height)
268
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800269 def set_rename_map(self):
270 """Initializes a dict `self.rename_map` for image renaming.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800271
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800272 For each items in the dict, image `key` will be renamed to `value`.
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800273 """
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800274 is_detachable = os.getenv('DETACHABLE') == '1'
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800275 physical_presence = os.getenv('PHYSICAL_PRESENCE')
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800276 rename_map = {}
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800277
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800278 # Navigation instructions
279 if is_detachable:
280 rename_map.update({
281 'nav-button_power': 'nav-key_enter',
282 'nav-button_volume_up': 'nav-key_up',
283 'nav-button_volume_down': 'nav-key_down',
284 'navigate0_tablet': 'navigate0',
285 'navigate1_tablet': 'navigate1',
286 })
287 else:
288 rename_map.update({
289 'nav-button_power': None,
290 'nav-button_volume_up': None,
291 'nav-button_volume_down': None,
292 'navigate0_tablet': None,
293 'navigate1_tablet': None,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800294 })
295
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800296 # Physical presence confirmation
297 if physical_presence == 'recovery':
298 rename_map['rec_to_dev_desc1_phyrec'] = 'rec_to_dev_desc1'
299 rename_map['rec_to_dev_desc1_power'] = None
300 elif physical_presence == 'power':
301 rename_map['rec_to_dev_desc1_phyrec'] = None
302 rename_map['rec_to_dev_desc1_power'] = 'rec_to_dev_desc1'
303 else:
304 rename_map['rec_to_dev_desc1_phyrec'] = None
305 rename_map['rec_to_dev_desc1_power'] = None
306 if physical_presence != 'keyboard':
307 raise BuildImageError('Invalid physical presence setting %s for board '
308 '%s' % (physical_presence, self.board))
309
310 # Broken screen
311 if physical_presence == 'recovery':
312 rename_map['broken_desc_phyrec'] = 'broken_desc'
313 rename_map['broken_desc_detach'] = None
314 elif is_detachable:
315 rename_map['broken_desc_phyrec'] = None
316 rename_map['broken_desc_detach'] = 'broken_desc'
317 else:
318 rename_map['broken_desc_phyrec'] = None
319 rename_map['broken_desc_detach'] = None
320
321 # SD card
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800322 if not self.config[KEY_SDCARD]:
Yu-Ping Wu3d272e72021-03-01 12:01:55 +0800323 rename_map.update({
324 'rec_sel_desc1_no_sd': 'rec_sel_desc1',
325 'rec_sel_desc1_no_phone_no_sd': 'rec_sel_desc1_no_phone',
326 'rec_disk_step1_desc0_no_sd': 'rec_disk_step1_desc0',
327 })
328 else:
329 rename_map.update({
330 'rec_sel_desc1_no_sd': None,
331 'rec_sel_desc1_no_phone_no_sd': None,
332 'rec_disk_step1_desc0_no_sd': None,
333 })
334
335 # Check for duplicate new names
336 new_names = list(new_name for new_name in rename_map.values() if new_name)
337 if len(set(new_names)) != len(new_names):
338 raise BuildImageError('Duplicate values found in rename_map')
339
340 # Map new_name to None to skip image generation for it
341 for new_name in new_names:
342 if new_name not in rename_map:
343 rename_map[new_name] = None
344
345 # Print mapping
346 print('Rename map:')
347 for name, new_name in sorted(rename_map.items()):
348 print(' %s => %s' % (name, new_name))
349
350 self.rename_map = rename_map
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800351
352 def set_locales(self):
353 """Sets a list of locales for which localized images are converted."""
354 # LOCALES environment variable can overwrite boards.yaml
355 env_locales = os.getenv('LOCALES')
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800356 rtl_locales = set(self.config[KEY_RTL])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800357 if env_locales:
358 locales = env_locales.split()
359 else:
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800360 locales = self.config[KEY_LOCALES]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800361 # Check rtl_locales are contained in locales.
362 unknown_rtl_locales = rtl_locales - set(locales)
363 if unknown_rtl_locales:
364 raise BuildImageError('Unknown locales %s in %s' %
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800365 (list(unknown_rtl_locales), KEY_RTL))
Yu-Ping Wuabb9afb2020-10-27 17:15:22 +0800366 self.locales = [LocaleInfo(code, code in rtl_locales)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800367 for code in locales]
368
Yu-Ping Wu96cf0022021-01-07 15:55:49 +0800369 @classmethod
370 def get_text_colors(cls, dpi):
371 """Derive maximum text colors from `dpi`."""
372 if dpi < 64:
373 return 2
374 elif dpi < 72:
375 return 3
376 elif dpi < 80:
377 return 4
378 elif dpi < 96:
379 return 5
380 elif dpi < 112:
381 return 6
382 else:
383 return 7
384
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800385 def _to_px(self, length, num_lines=1):
386 """Converts the relative coordinate to absolute one in pixels."""
Yu-Ping Wu3d07a062021-01-26 18:10:32 +0800387 return int(self.canvas_px * length / SCALE_BASE) * num_lines
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800388
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800389 def _get_png_height(self, png_file):
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800390 # With small DPI, pango-view may generate an empty file
391 if os.path.getsize(png_file) == 0:
392 return 0
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800393 with Image.open(png_file) as image:
394 return image.size[1]
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800395
396 def get_num_lines(self, file, one_line_dir):
397 """Gets the number of lines of text in `file`."""
398 name, _ = os.path.splitext(os.path.basename(file))
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800399 png_name = name + '.png'
400 multi_line_file = os.path.join(os.path.dirname(file), png_name)
401 one_line_file = os.path.join(one_line_dir, png_name)
402 # The number of lines is determined by comparing the height of
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800403 # `multi_line_file` with `one_line_file`, where the latter is generated
404 # without the '--width' option passed to pango-view.
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800405 height = self._get_png_height(multi_line_file)
406 line_height = self._get_png_height(one_line_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800407 return int(round(height / line_height))
408
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800409 def convert_svg_to_png(self, svg_file, png_file, height, bgcolor,
410 num_lines=1):
411 """Converts SVG to PNG file."""
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800412 # If the width/height of the SVG file is specified in points, the
413 # rsvg-convert command with default 90DPI will potentially cause the pixels
414 # at the right/bottom border of the output image to be transparent (or
415 # filled with the specified background color). This seems like an
416 # rsvg-convert issue regarding image scaling. Therefore, use 72DPI here
417 # to avoid the scaling.
418 command = ['rsvg-convert',
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800419 '--background-color', "'%s'" % bgcolor,
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800420 '--dpi-x', '72',
421 '--dpi-y', '72',
422 '-o', png_file]
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800423 height_px = self._to_px(height, num_lines)
Yu-Ping Wue445e042020-11-19 15:53:42 +0800424 if height_px <= 0:
425 raise BuildImageError('Height of %r <= 0 (%dpx)' %
426 (os.path.basename(svg_file), height_px))
427 command.extend(['--height', '%d' % height_px])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800428 command.append(svg_file)
429 subprocess.check_call(' '.join(command), shell=True)
430
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800431 def convert_png_to_bmp(self, png_file, bmp_file, max_colors, num_lines=1):
432 """Converts PNG to BMP file."""
433 image = Image.open(png_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800434
435 # Process alpha channel and transparency.
436 if image.mode == 'RGBA':
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800437 raise BuildImageError('PNG with RGBA mode is not supported')
438 elif image.mode == 'P' and 'transparency' in image.info:
439 raise BuildImageError('PNG with RGBA palette is not supported')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800440 elif image.mode != 'RGB':
441 target = image.convert('RGB')
442 else:
443 target = image
444
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800445 width_px, height_px = image.size
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800446 # Stretch image horizontally for stretched display.
Yu-Ping Wue445e042020-11-19 15:53:42 +0800447 if self.panel_stretch != 1:
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800448 width_px = int(width_px * self.panel_stretch)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800449 target = target.resize((width_px, height_px), Image.BICUBIC)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800450
451 # Export and downsample color space.
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800452 target.convert('P',
453 dither=None,
454 colors=max_colors,
455 palette=Image.ADAPTIVE).save(bmp_file)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800456
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800457 with open(bmp_file, 'rb+') as f:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800458 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
459 f.write(bytearray([num_lines]))
460
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800461 def _bisect_dpi(self, max_dpi, initial_dpi, max_height_px, get_height):
462 """Bisects to find the DPI that produces image height `max_height_px`.
463
464 Args:
465 max_dpi: Maximum DPI for binary search.
466 initial_dpi: Initial DPI to try with in binary search.
467 If specified, the value must be no larger than `max_dpi`.
468 max_height_px: Maximum (target) height to search for.
469 get_height: A function converting DPI to height. The function is called
470 once before returning.
471
472 Returns:
473 The best integer DPI within [1, `max_dpi`].
474 """
475
476 min_dpi = 1
477 first_iter = True
478
479 min_height_px = get_height(min_dpi)
480 if min_height_px > max_height_px:
481 # For some font such as "Noto Sans CJK SC", the generated height cannot
482 # go below a certain value. In this case, find max DPI with
483 # height_px <= min_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 + 1) // 2
489 height_px = get_height(mid_dpi)
490 if height_px > min_height_px:
491 max_dpi = mid_dpi - 1
492 else:
493 min_dpi = mid_dpi
494 first_iter = False
495 get_height(max_dpi)
496 return max_dpi
497
498 # Find min DPI with height_px == max_height_px
499 while min_dpi < max_dpi:
500 if first_iter and initial_dpi:
501 mid_dpi = initial_dpi
502 else:
503 mid_dpi = (min_dpi + max_dpi) // 2
504 height_px = get_height(mid_dpi)
505 if height_px == max_height_px:
506 return mid_dpi
507 elif height_px < max_height_px:
508 min_dpi = mid_dpi + 1
509 else:
510 max_dpi = mid_dpi
511 first_iter = False
512 get_height(min_dpi)
513 return min_dpi
514
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800515 def convert_text_to_image(self, locale, input_file, output_file, font,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800516 stage_dir, max_colors, height=None, max_width=None,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800517 dpi=None, initial_dpi=None,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800518 bgcolor='#000000', fgcolor='#ffffff',
519 use_svg=False):
520 """Converts text file `input_file` into image file.
521
522 Because pango-view does not support assigning output format options for
523 bitmap, we must create images in SVG/PNG format and then post-process them
524 (e.g. convert into BMP by ImageMagick).
525
526 Args:
527 locale: Locale (language) to select implicit rendering options. None for
528 locale-independent strings.
529 input_file: Path of input text file.
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800530 output_file: Path of output image file.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800531 font: Font name.
532 stage_dir: Directory to store intermediate file(s).
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800533 max_colors: Maximum colors to convert to bitmap.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800534 height: Image height relative to the screen resolution.
535 max_width: Maximum image width relative to the screen resolution.
536 dpi: DPI value passed to pango-view.
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800537 initial_dpi: Initial DPI to try with in binary search.
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800538 bgcolor: Background color (#rrggbb).
539 fgcolor: Foreground color (#rrggbb).
540 use_svg: If set to True, generate SVG file. Otherwise, generate PNG file.
541
542 Returns:
543 Effective DPI, or `None` when not applicable.
544 """
545 one_line_dir = os.path.join(stage_dir, ONE_LINE_DIR)
546 os.makedirs(one_line_dir, exist_ok=True)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800547
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800548 name, _ = os.path.splitext(os.path.basename(input_file))
549 svg_file = os.path.join(stage_dir, name + '.svg')
550 png_file = os.path.join(stage_dir, name + '.png')
551 png_file_one_line = os.path.join(one_line_dir, name + '.png')
552
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800553 def get_one_line_png_height(dpi):
554 """Generates a one-line PNG using DPI `dpi` and returns its height."""
555 run_pango_view(input_file, png_file_one_line, locale, font, height, 0,
556 dpi, bgcolor, fgcolor)
557 return self._get_png_height(png_file_one_line)
558
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800559 if use_svg:
560 run_pango_view(input_file, svg_file, locale, font, height, 0, dpi,
561 bgcolor, fgcolor, hinting='none')
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800562 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
563 self.convert_png_to_bmp(png_file, output_file, max_colors)
Yu-Ping Wub87a47d2021-03-30 14:10:22 +0800564 return None
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800565 else:
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800566 if not dpi:
567 raise BuildImageError('DPI must be specified with use_svg=False')
568
569 eff_dpi = dpi
Yu-Ping Wu75ffe692021-04-08 12:06:36 +0800570 max_height_px = self._to_px(height)
571 height_px = get_one_line_png_height(dpi)
572 if height_px > max_height_px:
573 eff_dpi = self._bisect_dpi(dpi, initial_dpi, max_height_px,
574 get_one_line_png_height)
575 if max_width:
576 # NOTE: With the same DPI, the height of multi-line PNG is not
577 # necessarily a multiple of the height of one-line PNG. Therefore, even
578 # with the binary search, the height of the resulting multi-line PNG
579 # might be less than "one_line_height * num_lines". We cannot
580 # binary-search DPI for multi-line PNGs because "num_lines" is dependent
581 # on DPI.
582 run_pango_view(input_file, png_file, locale, font, height, max_width,
583 eff_dpi, bgcolor, fgcolor)
584 num_lines = self.get_num_lines(png_file, one_line_dir)
585 else:
586 png_file = png_file_one_line
587 num_lines = 1
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800588 self.convert_png_to_bmp(png_file, output_file, max_colors,
589 num_lines=num_lines)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800590 return eff_dpi
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800591
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800592 def convert_sprite_images(self):
593 """Converts sprite images."""
594 names = self.formats[KEY_SPRITE_FILES]
595 styles = self.formats[KEY_STYLES]
596 # Check redundant images
Yu-Ping Wu20913672021-03-24 15:25:10 +0800597 for filename in glob.glob(os.path.join(self.sprite_dir, SVG_FILES)):
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800598 name, _ = os.path.splitext(os.path.basename(filename))
599 if name not in names:
600 raise BuildImageError('Sprite image %r not specified in %s' %
601 (filename, FORMAT_FILE))
602 # Convert images
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800603 for name, category in names.items():
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800604 new_name = self.rename_map.get(name, name)
605 if not new_name:
606 continue
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800607 style = get_config_with_defaults(styles, category)
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800608 svg_file = os.path.join(self.sprite_dir, name + '.svg')
609 png_file = os.path.join(self.temp_dir, name + '.png')
610 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800611 height = style[KEY_HEIGHT]
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800612 bgcolor = style[KEY_BGCOLOR]
613 self.convert_svg_to_png(svg_file, png_file, height, bgcolor)
614 self.convert_png_to_bmp(png_file, bmp_file, self.SPRITE_MAX_COLORS)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800615
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800616 def build_generic_strings(self):
617 """Builds images of generic (locale-independent) strings."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800618 dpi = self.config[KEY_DPI]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800619
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800620 names = self.formats[KEY_GENERIC_FILES]
621 styles = self.formats[KEY_STYLES]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800622 fonts = self.formats[KEY_FONTS]
623 default_font = fonts[KEY_DEFAULT]
624
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800625 for txt_file in glob.glob(os.path.join(self.strings_dir, '*.txt')):
626 name, _ = os.path.splitext(os.path.basename(txt_file))
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800627 new_name = self.rename_map.get(name, name)
628 if not new_name:
629 continue
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800630 bmp_file = os.path.join(self.output_dir, new_name + '.bmp')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800631 category = names[name]
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800632 style = get_config_with_defaults(styles, category)
Yu-Ping Wu75ffe692021-04-08 12:06:36 +0800633 if style[KEY_MAX_WIDTH]:
634 # Setting max_width causes left/right alignment of the text. However,
635 # generic strings are locale independent, and hence shouldn't have text
636 # alignment within the bitmap.
637 raise BuildImageError('{}: {!r} should be null for generic strings'
638 .format(name, KEY_MAX_WIDTH))
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800639 self.convert_text_to_image(None, txt_file, bmp_file, default_font,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800640 self.stage_dir, self.text_max_colors,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800641 height=style[KEY_HEIGHT],
Yu-Ping Wu75ffe692021-04-08 12:06:36 +0800642 max_width=None,
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800643 dpi=dpi,
644 bgcolor=style[KEY_BGCOLOR],
645 fgcolor=style[KEY_FGCOLOR])
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800646
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800647 def build_locale(self, locale, names, json_dir):
648 """Builds images of strings for `locale`."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800649 dpi = self.config[KEY_DPI]
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800650 styles = self.formats[KEY_STYLES]
651 fonts = self.formats[KEY_FONTS]
652 font = fonts.get(locale, fonts[KEY_DEFAULT])
653 inputs = parse_locale_json_file(locale, json_dir)
654
655 # Walk locale directory to add pre-generated texts such as language names.
656 for txt_file in glob.glob(os.path.join(self.locale_dir, locale, '*.txt')):
657 name, _ = os.path.splitext(os.path.basename(txt_file))
658 with open(txt_file, 'r', encoding='utf-8-sig') as f:
659 inputs[name] = f.read().strip()
660
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800661 stage_dir = os.path.join(self.stage_locale_dir, locale)
662 os.makedirs(stage_dir, exist_ok=True)
663 output_dir = os.path.join(self.output_ro_dir, locale)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800664 os.makedirs(output_dir, exist_ok=True)
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800665
666 eff_dpi_counters = defaultdict(Counter)
667 results = []
668 for name, category in sorted(names.items()):
669 # Ignore missing translation
670 if locale != 'en' and name not in inputs:
671 continue
672
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800673 new_name = self.rename_map.get(name, name)
674 if not new_name:
675 continue
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800676 output_file = os.path.join(output_dir, new_name + '.bmp')
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800677
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800678 # Write to text file
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800679 text_file = os.path.join(stage_dir, name + '.txt')
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800680 with open(text_file, 'w', encoding='utf-8-sig') as f:
681 f.write(inputs[name] + '\n')
682
683 # Convert text to image
684 style = get_config_with_defaults(styles, category)
685 height = style[KEY_HEIGHT]
686 eff_dpi_counter = eff_dpi_counters[height]
687 if eff_dpi_counter:
688 # Find the effective DPI that appears most times for `height`. This
689 # avoid doing the same binary search again and again. In case of a tie,
690 # pick the largest DPI.
691 best_eff_dpi = max(eff_dpi_counter,
692 key=lambda dpi: (eff_dpi_counter[dpi], dpi))
693 else:
694 best_eff_dpi = None
695 eff_dpi = self.convert_text_to_image(locale,
696 text_file,
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800697 output_file,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800698 font,
Yu-Ping Wu95493a92021-03-10 13:10:51 +0800699 stage_dir,
Yu-Ping Wu22dc45f2021-03-24 14:54:36 +0800700 self.text_max_colors,
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800701 height=height,
702 max_width=style[KEY_MAX_WIDTH],
703 dpi=dpi,
704 initial_dpi=best_eff_dpi,
705 bgcolor=style[KEY_BGCOLOR],
706 fgcolor=style[KEY_FGCOLOR])
707 eff_dpi_counter[eff_dpi] += 1
708 assert eff_dpi <= dpi
709 if eff_dpi != dpi:
710 results.append(eff_dpi)
711 return results
712
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800713 def _check_text_width(self, names):
714 """Checks if text image will exceed the expected drawing area at runtime."""
715 styles = self.formats[KEY_STYLES]
716
717 for locale_info in self.locales:
718 locale = locale_info.code
719 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800720 for filename in glob.glob(os.path.join(ro_locale_dir, '*.bmp')):
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800721 name, _ = os.path.splitext(os.path.basename(filename))
722 category = names[name]
723 style = get_config_with_defaults(styles, category)
724 height = style[KEY_HEIGHT]
725 max_width = style[KEY_MAX_WIDTH]
726 if not max_width:
727 continue
728 max_width_px = self._to_px(max_width)
729 with open(filename, 'rb') as f:
730 f.seek(BMP_HEADER_OFFSET_NUM_LINES)
731 num_lines = f.read(1)[0]
732 height_px = self._to_px(height * num_lines)
733 with Image.open(filename) as image:
734 width_px = height_px * image.size[0] // image.size[1]
735 if width_px > max_width_px:
736 raise BuildImageError('%s: Image width %dpx greater than max width '
737 '%dpx' % (filename, width_px, max_width_px))
Yu-Ping Wu08defcc2020-05-07 16:21:03 +0800738
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800739 def _copy_missing_bitmaps(self):
740 """Copy missing (not yet translated) strings from locale 'en'."""
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800741 en_files = glob.glob(os.path.join(self.output_ro_dir, 'en', '*.bmp'))
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800742 for locale_info in self.locales:
743 locale = locale_info.code
744 if locale == 'en':
745 continue
746 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
747 for en_file in en_files:
748 filename = os.path.basename(en_file)
749 locale_file = os.path.join(ro_locale_dir, filename)
750 if not os.path.isfile(locale_file):
751 print("WARNING: Locale '%s': copying '%s'" % (locale, filename))
752 shutil.copyfile(en_file, locale_file)
753
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800754 def build_localized_strings(self):
755 """Builds images of localized strings."""
756 # Sources are one .grd file with identifiers chosen by engineers and
757 # corresponding English texts, as well as a set of .xtb files (one for each
758 # language other than US English) with a mapping from hash to translation.
759 # Because the keys in the .xtb files are a hash of the English source text,
760 # rather than our identifiers, such as "btn_cancel", we use the "grit"
761 # command line tool to process the .grd and .xtb files, producing a set of
762 # .json files mapping our identifier to the translated string, one for every
763 # language including US English.
764
765 # Create a temporary directory to place the translation output from grit in.
766 json_dir = tempfile.mkdtemp()
767
768 # This invokes the grit build command to generate JSON files from the XTB
769 # files containing translations. The results are placed in `json_dir` as
770 # specified in firmware_strings.grd, i.e. one JSON file per locale.
771 subprocess.check_call([
772 'grit',
773 '-i', os.path.join(self.locale_dir, STRINGS_GRD_FILE),
774 'build',
775 '-o', os.path.join(json_dir),
776 ])
777
778 # Make a copy to avoid modifying `self.formats`
779 names = copy.deepcopy(self.formats[KEY_LOCALIZED_FILES])
780 if DIAGNOSTIC_UI:
781 names.update(self.formats[KEY_DIAGNOSTIC_FILES])
782
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800783 executor = ProcessPoolExecutor()
784 futures = []
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800785 for locale_info in self.locales:
786 locale = locale_info.code
787 print(locale, end=' ', flush=True)
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800788 futures.append(executor.submit(self.build_locale, locale, names,
789 json_dir))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800790
791 print()
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800792
793 try:
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800794 results = [future.result() for future in futures]
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800795 except KeyboardInterrupt:
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800796 executor.shutdown(wait=False)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800797 exit('Aborted by user')
798 else:
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800799 executor.shutdown()
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800800
Yu-Ping Wu49606eb2021-03-03 22:43:19 +0800801 effective_dpi = [dpi for r in results for dpi in r if dpi]
Yu-Ping Wuf946dd42021-02-08 16:32:28 +0800802 if effective_dpi:
803 print('Reducing effective DPI to %d, limited by screen resolution' %
804 max(effective_dpi))
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800805
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800806 shutil.rmtree(json_dir)
Yu-Ping Wu2e788b02021-03-09 13:01:31 +0800807 self._check_text_width(names)
Yu-Ping Wu703dcfd2021-01-08 10:52:10 +0800808 self._copy_missing_bitmaps()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800809
810 def move_language_images(self):
811 """Renames language bitmaps and move to self.output_dir.
812
813 The directory self.output_dir contains locale-independent images, and is
814 used for creating vbgfx.bin by archive_images.py.
815 """
816 for locale_info in self.locales:
817 locale = locale_info.code
818 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
819 old_file = os.path.join(ro_locale_dir, 'language.bmp')
820 new_file = os.path.join(self.output_dir, 'language_%s.bmp' % locale)
821 if os.path.exists(new_file):
822 raise BuildImageError('File already exists: %s' % new_file)
823 shutil.move(old_file, new_file)
824
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800825 def build_glyphs(self):
826 """Builds glyphs of ascii characters."""
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800827 os.makedirs(self.stage_glyph_dir, exist_ok=True)
828 output_dir = os.path.join(self.output_dir, 'glyph')
829 os.makedirs(output_dir)
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800830 executor = ProcessPoolExecutor()
831 futures = []
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800832 for c in range(ord(' '), ord('~') + 1):
833 name = f'idx{c:03d}_{c:02x}'
Yu-Ping Wu31a6e6b2021-03-24 15:08:53 +0800834 txt_file = os.path.join(self.stage_glyph_dir, name + '.txt')
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800835 with open(txt_file, 'w', encoding='ascii') as f:
836 f.write(chr(c))
837 f.write('\n')
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800838 output_file = os.path.join(output_dir, name + '.bmp')
Yu-Ping Wufc1f4b12021-03-30 14:10:15 +0800839 futures.append(executor.submit(self.convert_text_to_image, None, txt_file,
840 output_file, GLYPH_FONT,
841 self.stage_glyph_dir,
842 self.GLYPH_MAX_COLORS,
843 height=DEFAULT_GLYPH_HEIGHT,
844 use_svg=True))
845 for future in futures:
846 future.result()
847 executor.shutdown()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800848
849 def copy_images_to_rw(self):
850 """Copies localized images specified in boards.yaml for RW override."""
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800851 if not self.config[KEY_RW_OVERRIDE]:
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800852 print(' No localized images are specified for RW, skipping')
853 return
854
855 for locale_info in self.locales:
856 locale = locale_info.code
Chung-Sheng Wucd3b4e22021-04-01 18:50:20 +0800857 ro_locale_dir = os.path.join(self.output_ro_dir, locale)
858 rw_locale_dir = os.path.join(self.output_rw_dir, locale)
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800859 os.makedirs(rw_locale_dir)
860
Yu-Ping Wu60b45372021-03-31 16:56:08 +0800861 for name in self.config[KEY_RW_OVERRIDE]:
Yu-Ping Wu879d1d32021-04-08 15:48:48 +0800862 ro_src = os.path.join(ro_locale_dir, name + '.bmp')
863 rw_dst = os.path.join(rw_locale_dir, name + '.bmp')
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800864 shutil.copyfile(ro_src, rw_dst)
865
866 def create_locale_list(self):
867 """Creates locale list as a CSV file.
868
869 Each line in the file is of format "code,rtl", where
870 - "code": language code of the locale
871 - "rtl": "1" for right-to-left language, "0" otherwise
872 """
873 with open(os.path.join(self.output_dir, 'locales'), 'w') as f:
874 for locale_info in self.locales:
875 f.write('{},{}\n'.format(locale_info.code,
876 int(locale_info.rtl)))
877
878 def build(self):
879 """Builds all images required by a board."""
880 # Clean up output directory
881 if os.path.exists(self.output_dir):
882 shutil.rmtree(self.output_dir)
883 os.makedirs(self.output_dir)
884
885 if not os.path.exists(self.stage_dir):
886 raise BuildImageError('Missing stage folder. Run make in strings dir.')
887
888 # Clean up temp directory
889 if os.path.exists(self.temp_dir):
890 shutil.rmtree(self.temp_dir)
891 os.makedirs(self.temp_dir)
892
Yu-Ping Wu177f12c2020-11-04 15:55:37 +0800893 print('Converting sprite images...')
894 self.convert_sprite_images()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800895
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800896 print('Building generic strings...')
897 self.build_generic_strings()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800898
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800899 print('Building localized strings...')
900 self.build_localized_strings()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800901
902 print('Moving language images to locale-independent directory...')
903 self.move_language_images()
904
905 print('Creating locale list file...')
906 self.create_locale_list()
907
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800908 print('Building glyphs...')
909 self.build_glyphs()
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800910
911 print('Copying specified images to RW packing directory...')
912 self.copy_images_to_rw()
913
914
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800915def main():
916 """Builds bitmaps for firmware screens."""
917 parser = argparse.ArgumentParser()
918 parser.add_argument('board', help='Target board')
919 args = parser.parse_args()
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800920 board = args.board
Yu-Ping Wu8c8bfc72020-10-27 16:19:34 +0800921
922 with open(FORMAT_FILE, encoding='utf-8') as f:
923 formats = yaml.load(f)
Yu-Ping Wuc00e1712021-04-13 16:44:12 +0800924 board_config = load_board_config(BOARDS_CONFIG_FILE, board)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800925
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800926 print('Building for ' + board)
Yu-Ping Wu675e7e82021-01-29 08:32:12 +0800927 check_fonts(formats[KEY_FONTS])
928 print('Output dir: ' + OUTPUT_DIR)
Yu-Ping Wue66a7b02020-11-19 15:18:08 +0800929 converter = Converter(board, formats, board_config, OUTPUT_DIR)
930 converter.build()
Yu-Ping Wu7f6639a2020-09-28 15:31:35 +0800931
932
Hung-Te Lin707e2ef2013-08-06 10:20:04 +0800933if __name__ == '__main__':
Yu-Ping Wu6e4d3892020-10-19 14:09:37 +0800934 main()