Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions bricks/_common/common.mk
Original file line number Diff line number Diff line change
Expand Up @@ -552,9 +552,31 @@ ifneq ($(PB_MCU_FAMILY),TIAM1808)
SRC_S += lib/pbio/platform/$(PBIO_PLATFORM)/startup.s
endif

vpath %.bmp $(PBTOP)
vpath %.jpg $(PBTOP)
vpath %.png $(PBTOP)
vpath %.svg $(PBTOP)
MEDIA_SRC = $(sort $(addprefix lib/pbio/src/image/media/,\
lms2012/_app_ir_control_12.bmp \
lms2012/_app_ir_control_34.bmp \
lms2012/_app_motor_control_ad.bmp \
lms2012/_app_motor_control_bc.bmp \
ui/_accept24_fill.svg \
ui/_accept24.svg \
ui/_off20.svg \
ui/_pybricks_join.png \
ui/_reject24_fill.svg \
ui/_reject24.svg \
ui/_rotate_ccw18.svg \
ui/_rotate_cw18.svg \
ui/_usb_host.svg \
ui/_wrench17.svg \
))
MEDIA_GEN_C = $(patsubst lib/pbio/src/image/media/%, $(BUILD)/media/%.c, $(basename $(MEDIA_SRC)))

ifeq ($(PB_MEDIA),1)
PYBRICKS_PYBRICKS_SRC_C += $(BUILD)/pb_type_image_attributes.c
PBIO_SRC_C += $(BUILD)/pbio_image_media.c
PBIO_SRC_C += $(MEDIA_GEN_C)
PBIO_SRC_C += $(BUILD)/hmi_ev3_ui_credits.c
endif

Expand Down Expand Up @@ -679,9 +701,38 @@ else
FW_SECTIONS :=
endif

$(BUILD)/pbio_image_media.c $(BUILD)/pb_type_image_attributes.c: $(MEDIA_CONVERT)
# Force media list regeneration if list of media sources changed.
-include $(BUILD)/media_src_gen.mk
MEDIA_REGEN := $(if $(strip $(filter-out $(MEDIA_SRC),$(MEDIA_SRC_GEN)) $(filter-out $(MEDIA_SRC_GEN),$(MEDIA_SRC))),media-regen)
media-regen:
.PHONY: media-regen
.SECONDARY: $(MEDIA_GEN_C)

$(BUILD)/media/%.c: lib/pbio/src/image/media/%.bmp $(MEDIA_CONVERT)
$(ECHO) "GEN $@"
$(Q)mkdir -p $(dir $@)
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<

$(BUILD)/media/%.c: lib/pbio/src/image/media/%.jpg $(MEDIA_CONVERT)
$(ECHO) "GEN $@"
$(Q)mkdir -p $(dir $@)
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<

$(BUILD)/media/%.c: lib/pbio/src/image/media/%.png $(MEDIA_CONVERT)
$(ECHO) "GEN $@"
$(Q)mkdir -p $(dir $@)
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<

$(BUILD)/media/%.c: lib/pbio/src/image/media/%.svg $(MEDIA_CONVERT)
$(ECHO) "GEN $@"
$(Q)mkdir -p $(dir $@)
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<

$(BUILD)/pbio_image_media.h $(BUILD)/pb_type_image_attributes.c &: $(MEDIA_CONVERT) $(MEDIA_REGEN)
$(ECHO) "Generating image media files"
$(Q)$(PYTHON) $(MEDIA_CONVERT) $(BUILD)
$(Q)mkdir -p $(BUILD)
$(Q)$(PYTHON) $(MEDIA_CONVERT) --decls $(BUILD)/pbio_image_media.h --attrs $(BUILD)/pb_type_image_attributes.c $(MEDIA_SRC)
$(ECHO) "MEDIA_SRC_GEN = $(MEDIA_SRC)" > $(BUILD)/media_src_gen.mk

$(BUILD)/font_liberationsans_regular_14.c: $(PBTOP)/lib/pbio/src/image/fonts/LiberationSans-Regular.ttf $(FONT_CONVERT)
$(ECHO) "GEN $@"
Expand Down
157 changes: 83 additions & 74 deletions lib/pbio/src/image/media.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
#!/usr/bin/env python3
import argparse
import sys
import io
from pathlib import Path
from PIL import Image
import cairosvg

# Take build directory as argument to save generated C files and PNG files.
parser = argparse.ArgumentParser(description="Convert image files to C structs.")
parser.add_argument("dest", help="Destination build folder for PNG files.")
parser.add_argument("-o", "--output", type=Path, help="Output file name for C structs.")
parser.add_argument(
"--decls", type=Path, help="Output file name for struct declarations."
)
parser.add_argument(
"--decls-include",
help="Struct declaration include file name, generated from output file if given.",
)
parser.add_argument("--attrs", type=Path, help="Output file name for attribute list.")
parser.add_argument("images", type=Path, nargs="+", help="Input file name(s).")
args = parser.parse_args()

build_dir = Path(args.dest)
build_dir.mkdir(parents=True, exist_ok=True)
media_dir = Path(__file__).parent / "media"

# Convert all SVG files in media_dir to PNG and save in build_dir if not already present.
svg_files = media_dir.rglob("*.svg")
for svg in svg_files:
png = svg.with_suffix(".png").name
png_path = build_dir / png
if png_path.exists():
continue
with open(svg, "rb") as svg_file:
png_bytes = cairosvg.svg2png(file_obj=svg_file)
with open(png_path, "wb") as out_png:
out_png.write(png_bytes)

# Collect all image files in media_dir (png, bmp, jpg) and build_dir (png), including subfolders.
media_images = (
list(media_dir.rglob("*.png"))
+ list(media_dir.rglob("*.bmp"))
+ list(media_dir.rglob("*.jpg"))
+ list(build_dir.rglob("*.png"))
)
if args.attrs:
if args.decls_include is not None:
decls_include = args.decls_include
elif args.decls is not None:
decls_include = args.decls.name
else:
parser.error("Need struct declaration include file name.")


def load_image(path):
if path.suffix == ".svg":
with open(path, "rb") as f:
png_bytes = cairosvg.svg2png(file_obj=f)
return Image.open(io.BytesIO(png_bytes))
else:
return Image.open(path)


# Convert rgba to monochrome, treating fully transparent pixels as white.
Expand Down Expand Up @@ -61,11 +64,14 @@ def image_to_8bit_map(img):

# Process each image.
results = {}
for img_path in media_images:
with Image.open(img_path) as img:
name = Path(img_path.name).stem
width, height, bin_data = image_to_8bit_map(img)
results[name] = (width, height, bin_data)
for img_path in args.images:
name = img_path.stem
if args.output:
with load_image(img_path) as img:
width, height, bin_data = image_to_8bit_map(img)
results[name] = (width, height, bin_data)
else:
results[name] = (0, 0, None)


externs = ""
Expand All @@ -75,57 +81,60 @@ def image_to_8bit_map(img):
for name in sorted(results):
width, height, bin_data = results[name]

# Parse bytes for printing.
bytes_per_line = 12
lines = []
for i in range(0, len(bin_data), bytes_per_line):
chunk = bin_data[i : i + bytes_per_line]
line = " " + ", ".join(f"0x{val:02x}" for val in chunk)
lines.append(line)
data_literal = ",\n".join(lines) + ","

# Printed C structs.
structs += f"static const uint8_t {name}_data[] = {{\n{data_literal}\n}};\n\n"
structs += (
f"const pbio_image_monochrome_t pbio_image_media_{name} = {{\n"
f" .width = {width},\n"
f" .height = {height},\n"
f" .data = {name}_data,\n"
f"}};\n"
)
if args.output:
# Parse bytes for printing.
bytes_per_line = 12
lines = []
for i in range(0, len(bin_data), bytes_per_line):
chunk = bin_data[i : i + bytes_per_line]
line = " " + ", ".join(f"0x{val:02x}" for val in chunk)
lines.append(line)
data_literal = ",\n".join(lines) + ","

# Printed C structs.
structs += f"static const uint8_t {name}_data[] = {{\n{data_literal}\n}};\n\n"
structs += (
f"const pbio_image_monochrome_t pbio_image_media_{name} = {{\n"
f" .width = {width},\n"
f" .height = {height},\n"
f" .data = {name}_data,\n"
f"}};\n"
)

# Printed header and QSTR table entries.
externs += f"extern const pbio_image_monochrome_t pbio_image_media_{name};\n\n"
qstrtab += f" {{ MP_ROM_QSTR(MP_QSTR_{name.upper()}), MP_ROM_PTR(&pbio_image_media_{name}) }},\n"


HEADER = """// SPDX-License-Identifier: MIT
//Copyright (c) 2025 The Pybricks Authors
HEADER = """// Generated file, edit with care.

#include <pbio/image.h>
"""

with open(build_dir / "pbio_image_media.c", "w") as f:
f.write(HEADER)
f.write('#include "pbio_image_media.h"\n\n')
f.write(structs)

with open(build_dir / "pbio_image_media.h", "w") as f:
f.write(HEADER)
f.write("#ifndef _PBIO_IMAGE_MEDIA_H_\n")
f.write("#define _PBIO_IMAGE_MEDIA_H_\n\n")
f.write(externs)
f.write("#endif // _PBIO_IMAGE_MEDIA_H_\n")

with open(build_dir / "pb_type_image_attributes.c", "w") as f:
f.write(HEADER)
f.write('#include "pbio_image_media.h"\n\n')
f.write("#include <py/obj.h>\n\n")
f.write(
"static const mp_rom_map_elem_t pb_type_image_attributes_dict_table[] = {\n"
)
f.write(qstrtab)
f.write("};\n")
f.write(
"MP_DEFINE_CONST_DICT(pb_type_image_attributes_dict, pb_type_image_attributes_dict_table);"
)
if args.output:
with open(args.output, "w") as f:
f.write(HEADER)
f.write(structs)

if args.decls:
guard = args.decls.name.upper().replace(".", "_")
with open(args.decls, "w") as f:
f.write(HEADER)
f.write(f"#ifndef _{guard}_\n")
f.write(f"#define _{guard}_\n\n")
f.write(externs)
f.write(f"#endif // _{guard}_\n")

if args.attrs:
with open(args.attrs, "w") as f:
f.write(HEADER)
f.write(f'#include "{decls_include}"\n\n')
f.write("#include <py/obj.h>\n\n")
f.write(
"static const mp_rom_map_elem_t pb_type_image_attributes_dict_table[] = {\n"
)
f.write(qstrtab)
f.write("};\n")
f.write(
"MP_DEFINE_CONST_DICT(pb_type_image_attributes_dict, pb_type_image_attributes_dict_table);"
)
Loading