1
0
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:
matejcik 2019-11-28 14:44:00 +01:00 committed by matejcik
parent be29f20159
commit a46fd6f508
25 changed files with 248 additions and 3 deletions

View File

@ -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.

91
core/tools/build_icons.py Executable file
View 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()

View 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)

View File

@ -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),