mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-21 23:18:13 +00:00
core: auto-generate FIDO icons
This commit is contained in:
parent
be29f20159
commit
a46fd6f508
10
Makefile
10
Makefile
@ -65,14 +65,20 @@ templates: ## rebuild coin lists from definitions in common
|
||||
templates_check: ## check that coin lists are up to date
|
||||
./core/tools/build_templates --check
|
||||
|
||||
icons: ## generate FIDO service icons
|
||||
python3 core/tools/build_icons.py
|
||||
|
||||
icons_check: ## generate FIDO service icons
|
||||
python3 core/tools/build_icons.py --check
|
||||
|
||||
protobuf: ## generate python protobuf headers
|
||||
./tools/build_protobuf
|
||||
|
||||
protobuf_check: ## check that generated protobuf headers are up to date
|
||||
./tools/build_protobuf --check
|
||||
|
||||
gen: mocks templates protobuf ## regeneate auto-generated files from sources
|
||||
gen: mocks templates protobuf icons ## regeneate auto-generated files from sources
|
||||
make -C python coins_json
|
||||
|
||||
gen_check: mocks_check templates_check protobuf_check ## check validity of auto-generated files
|
||||
gen_check: mocks_check templates_check protobuf_check icons_check ## check validity of auto-generated files
|
||||
make -C python coins_json_check
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/src/apps/webauthn/res/icon_bitwarden.toif
Normal file
BIN
core/src/apps/webauthn/res/icon_bitwarden.toif
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/src/apps/webauthn/res/icon_slushpool.toif
Normal file
BIN
core/src/apps/webauthn/res/icon_slushpool.toif
Normal file
Binary file not shown.
Binary file not shown.
91
core/tools/build_icons.py
Executable file
91
core/tools/build_icons.py
Executable file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import click
|
||||
from PIL import Image
|
||||
|
||||
from trezorlib._internal import toif
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
|
||||
ICON_SIZE = (64, 64)
|
||||
DESTINATION = ROOT / "core" / "src" / "apps" / "webauthn" / "res"
|
||||
EXCLUDE = {"icon_webauthn"}
|
||||
|
||||
# insert ../../common/tools to sys.path, so that we can import coin_info
|
||||
# XXX this is hacky, but we want to keep coin_info in the common/ subdir for the purpose
|
||||
# of exporting it to e.g. Connect
|
||||
# And making a special python package out of it seems needless
|
||||
|
||||
COMMON_TOOLS_PATH = ROOT / "common" / "tools"
|
||||
sys.path.insert(0, str(COMMON_TOOLS_PATH))
|
||||
|
||||
import coin_info
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("-c", "--check", is_flag=True, help="Do not write, only check.")
|
||||
@click.option("-r", "--remove", is_flag=True, help="Remove unrecognized files.")
|
||||
def build_icons(check, remove):
|
||||
"""Build FIDO app icons in the source tree."""
|
||||
|
||||
checks_ok = True
|
||||
apps = coin_info.fido_info()
|
||||
|
||||
total_size = 0
|
||||
|
||||
for app in apps:
|
||||
if app["icon"] is None:
|
||||
if not app.get("demo"):
|
||||
raise click.ClickException(f"Icon not found for: {app['key']}")
|
||||
else:
|
||||
continue
|
||||
|
||||
im = Image.open(app["icon"])
|
||||
resized = im.resize(ICON_SIZE, Image.BOX)
|
||||
toi = toif.from_image(resized)
|
||||
dest_path = DESTINATION / f"icon_{app['key']}.toif"
|
||||
|
||||
total_size += len(toi.to_bytes())
|
||||
|
||||
if not check:
|
||||
toi.save(dest_path)
|
||||
else:
|
||||
if not dest_path.exists():
|
||||
print(f"Missing TOIF: {dest_path}")
|
||||
checks_ok = False
|
||||
continue
|
||||
data = dest_path.read_bytes()
|
||||
if data != toi.to_bytes():
|
||||
print(f"Icon different from source: {dest_path}")
|
||||
checks_ok = False
|
||||
|
||||
print(f"Total icon size: {total_size} bytes")
|
||||
|
||||
keys = EXCLUDE | {"icon_" + app["key"] for app in apps}
|
||||
unrecognized_files = False
|
||||
for icon_file in DESTINATION.glob("*.toif"):
|
||||
name = icon_file.stem
|
||||
if name not in keys:
|
||||
unrecognized_files = True
|
||||
if remove:
|
||||
print(f"Removing unrecognized file: {icon_file}")
|
||||
icon_file.unlink()
|
||||
else:
|
||||
print(f"Unrecognized file: {icon_file}")
|
||||
checks_ok = False
|
||||
|
||||
if not remove and unrecognized_files:
|
||||
raise click.ClickException(
|
||||
"Unrecognized files found in icon directory.\n"
|
||||
"Use 'build_icons.py -r' to remove them automatically."
|
||||
)
|
||||
if not checks_ok:
|
||||
raise click.ClickException("Some checks have failed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_icons()
|
128
python/src/trezorlib/_internal/toif.py
Normal file
128
python/src/trezorlib/_internal/toif.py
Normal file
@ -0,0 +1,128 @@
|
||||
import struct
|
||||
import zlib
|
||||
from typing import Sequence, Tuple
|
||||
|
||||
import attr
|
||||
from PIL import Image
|
||||
|
||||
from .. import firmware
|
||||
|
||||
RGBPixel = Tuple[int, int, int]
|
||||
|
||||
|
||||
def _compress(data: bytes) -> bytes:
|
||||
z = zlib.compressobj(level=9, wbits=-10)
|
||||
return z.compress(data) + z.flush()
|
||||
|
||||
|
||||
def _decompress(data: bytes) -> bytes:
|
||||
return zlib.decompress(data, wbits=-10)
|
||||
|
||||
|
||||
def _from_pil_rgb(pixels: Sequence[RGBPixel]) -> bytes:
|
||||
data = bytearray()
|
||||
for r, g, b in pixels:
|
||||
c = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)
|
||||
data += struct.pack(">H", c)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def _to_rgb(data: bytes) -> bytes:
|
||||
res = bytearray()
|
||||
for i in range(0, len(data), 2):
|
||||
(c,) = struct.unpack(">H", data[i : i + 2])
|
||||
r = (c & 0xF800) >> 8
|
||||
g = (c & 0x07C0) >> 3
|
||||
b = (c & 0x001F) << 3
|
||||
res += bytes((r, g, b))
|
||||
return bytes(res)
|
||||
|
||||
|
||||
def _from_pil_grayscale(pixels: Sequence[int]) -> bytes:
|
||||
data = bytearray()
|
||||
for i in range(0, len(pixels), 2):
|
||||
left, right = pixels[i], pixels[i + 1]
|
||||
c = (left & 0xF0) | ((right & 0xF0) >> 4)
|
||||
data += struct.pack(">B", c)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def _to_grayscale(data: bytes) -> bytes:
|
||||
res = bytearray()
|
||||
for pixel in data:
|
||||
left = pixel & 0xF0
|
||||
right = (pixel & 0x0F) << 4
|
||||
res += bytes((left, right))
|
||||
return bytes(res)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Toif:
|
||||
mode = attr.ib() # type: firmware.ToifMode
|
||||
size = attr.ib() # type: Tuple[int, int]
|
||||
data = attr.ib() # type: bytes
|
||||
|
||||
def _expected_data_length(self) -> int:
|
||||
width, height = self.size
|
||||
if self.mode is firmware.ToifMode.grayscale:
|
||||
return width * height // 2
|
||||
else:
|
||||
return width * height * 2
|
||||
|
||||
def to_image(self) -> Image:
|
||||
uncompressed = _decompress(self.data)
|
||||
expected_size = self._expected_data_length()
|
||||
if len(uncompressed) != expected_size:
|
||||
raise ValueError(
|
||||
"Uncompressed data is {} bytes, expected {}".format(
|
||||
len(uncompressed), expected_size
|
||||
)
|
||||
)
|
||||
|
||||
if self.mode is firmware.ToifMode.grayscale:
|
||||
pil_mode = "L"
|
||||
raw_data = _to_grayscale(uncompressed)
|
||||
else:
|
||||
pil_mode = "RGB"
|
||||
raw_data = _to_rgb(uncompressed)
|
||||
|
||||
return Image.frombuffer(pil_mode, self.size, raw_data, "raw", pil_mode, 0, 1)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
width, height = self.size
|
||||
return firmware.Toif.build(
|
||||
dict(format=self.mode, width=width, height=height, data=self.data)
|
||||
)
|
||||
|
||||
def save(self, filename: str) -> None:
|
||||
with open(filename, "wb") as out:
|
||||
out.write(self.to_bytes())
|
||||
|
||||
|
||||
def from_bytes(data: bytes) -> Toif:
|
||||
parsed = firmware.Toif.parse(data)
|
||||
return Toif(parsed.format, (parsed.width, parsed.height), parsed.data)
|
||||
|
||||
|
||||
def load(filename: str) -> Toif:
|
||||
with open(filename, "rb") as f:
|
||||
return from_bytes(f.read())
|
||||
|
||||
|
||||
def from_image(image: Image, background=(0, 0, 0, 255)) -> Toif:
|
||||
if image.mode == "RGBA":
|
||||
background = Image.new("RGBA", image.size, background)
|
||||
blend = Image.alpha_composite(background, image)
|
||||
image = blend.convert("RGB")
|
||||
|
||||
if image.mode == "L":
|
||||
toif_mode = firmware.ToifMode.grayscale
|
||||
toif_data = _from_pil_grayscale(image.getdata())
|
||||
elif image.mode == "RGB":
|
||||
toif_mode = firmware.ToifMode.full_color
|
||||
toif_data = _from_pil_rgb(image.getdata())
|
||||
else:
|
||||
raise ValueError("Unsupported image mode: {}".format(image.mode))
|
||||
|
||||
data = _compress(toif_data)
|
||||
return Toif(toif_mode, image.size, data)
|
@ -75,10 +75,30 @@ class Unsigned(FirmwareIntegrityError):
|
||||
pass
|
||||
|
||||
|
||||
class ToifMode(Enum):
|
||||
full_color = b"f"
|
||||
grayscale = b"g"
|
||||
|
||||
|
||||
class EnumAdapter(c.Adapter):
|
||||
def __init__(self, subcon, enum):
|
||||
self.enum = enum
|
||||
super().__init__(subcon)
|
||||
|
||||
def _encode(self, obj, ctx, path):
|
||||
return obj.value
|
||||
|
||||
def _decode(self, obj, ctx, path):
|
||||
try:
|
||||
return self.enum(obj)
|
||||
except ValueError:
|
||||
return obj
|
||||
|
||||
|
||||
# fmt: off
|
||||
Toif = c.Struct(
|
||||
"magic" / c.Const(b"TOI"),
|
||||
"format" / c.Enum(c.Byte, full_color=b"f", grayscale=b"g"),
|
||||
"format" / EnumAdapter(c.Bytes(1), ToifMode),
|
||||
"width" / c.Int16ul,
|
||||
"height" / c.Int16ul,
|
||||
"data" / c.Prefixed(c.Int32ul, c.GreedyBytes),
|
||||
|
Loading…
Reference in New Issue
Block a user