Skip to content

Commit 9274d09

Browse files
committed
pbio/image: Add dependencies for generated images.
Four changes: - stop relying on globing to select the media files, making the build more robust, - generate media in separated files, lowering the work to be done when only one file changed, - make sure that dependencies are specified in the Makefile, output files need to change when one of the source media file changed, - make sure that the list of source media is checked, rebuild is needed when the list change. This fixes spurious build failures when switching branches for example.
1 parent 11f88f3 commit 9274d09

2 files changed

Lines changed: 137 additions & 77 deletions

File tree

bricks/_common/common.mk

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,9 +552,31 @@ ifneq ($(PB_MCU_FAMILY),TIAM1808)
552552
SRC_S += lib/pbio/platform/$(PBIO_PLATFORM)/startup.s
553553
endif
554554

555+
vpath %.bmp $(PBTOP)
556+
vpath %.jpg $(PBTOP)
557+
vpath %.png $(PBTOP)
558+
vpath %.svg $(PBTOP)
559+
MEDIA_SRC = $(sort $(addprefix lib/pbio/src/image/media/,\
560+
lms2012/_app_ir_control_12.bmp \
561+
lms2012/_app_ir_control_34.bmp \
562+
lms2012/_app_motor_control_ad.bmp \
563+
lms2012/_app_motor_control_bc.bmp \
564+
ui/_accept24_fill.svg \
565+
ui/_accept24.svg \
566+
ui/_off20.svg \
567+
ui/_pybricks_join.png \
568+
ui/_reject24_fill.svg \
569+
ui/_reject24.svg \
570+
ui/_rotate_ccw18.svg \
571+
ui/_rotate_cw18.svg \
572+
ui/_usb_host.svg \
573+
ui/_wrench17.svg \
574+
))
575+
MEDIA_GEN_C = $(patsubst lib/pbio/src/image/media/%, $(BUILD)/media/%.c, $(basename $(MEDIA_SRC)))
576+
555577
ifeq ($(PB_MEDIA),1)
556578
PYBRICKS_PYBRICKS_SRC_C += $(BUILD)/pb_type_image_attributes.c
557-
PBIO_SRC_C += $(BUILD)/pbio_image_media.c
579+
PBIO_SRC_C += $(MEDIA_GEN_C)
558580
PBIO_SRC_C += $(BUILD)/hmi_ev3_ui_credits.c
559581
endif
560582

@@ -679,9 +701,38 @@ else
679701
FW_SECTIONS :=
680702
endif
681703

682-
$(BUILD)/pbio_image_media.c $(BUILD)/pb_type_image_attributes.c: $(MEDIA_CONVERT)
704+
# Force media list regeneration if list of media sources changed.
705+
-include $(BUILD)/media_src_gen.mk
706+
MEDIA_REGEN := $(if $(strip $(filter-out $(MEDIA_SRC),$(MEDIA_SRC_GEN)) $(filter-out $(MEDIA_SRC_GEN),$(MEDIA_SRC))),media-regen)
707+
media-regen:
708+
.PHONY: media-regen
709+
.SECONDARY: $(MEDIA_GEN_C)
710+
711+
$(BUILD)/media/%.c: lib/pbio/src/image/media/%.bmp $(MEDIA_CONVERT)
712+
$(ECHO) "GEN $@"
713+
$(Q)mkdir -p $(dir $@)
714+
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<
715+
716+
$(BUILD)/media/%.c: lib/pbio/src/image/media/%.jpg $(MEDIA_CONVERT)
717+
$(ECHO) "GEN $@"
718+
$(Q)mkdir -p $(dir $@)
719+
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<
720+
721+
$(BUILD)/media/%.c: lib/pbio/src/image/media/%.png $(MEDIA_CONVERT)
722+
$(ECHO) "GEN $@"
723+
$(Q)mkdir -p $(dir $@)
724+
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<
725+
726+
$(BUILD)/media/%.c: lib/pbio/src/image/media/%.svg $(MEDIA_CONVERT)
727+
$(ECHO) "GEN $@"
728+
$(Q)mkdir -p $(dir $@)
729+
$(Q)$(PYTHON) $(MEDIA_CONVERT) -o $@ $<
730+
731+
$(BUILD)/pbio_image_media.h $(BUILD)/pb_type_image_attributes.c &: $(MEDIA_CONVERT) $(MEDIA_REGEN)
683732
$(ECHO) "Generating image media files"
684-
$(Q)$(PYTHON) $(MEDIA_CONVERT) $(BUILD)
733+
$(Q)mkdir -p $(BUILD)
734+
$(Q)$(PYTHON) $(MEDIA_CONVERT) --decls $(BUILD)/pbio_image_media.h --attrs $(BUILD)/pb_type_image_attributes.c $(MEDIA_SRC)
735+
$(ECHO) "MEDIA_SRC_GEN = $(MEDIA_SRC)" > $(BUILD)/media_src_gen.mk
685736

686737
$(BUILD)/font_liberationsans_regular_14.c: $(PBTOP)/lib/pbio/src/image/fonts/LiberationSans-Regular.ttf $(FONT_CONVERT)
687738
$(ECHO) "GEN $@"

lib/pbio/src/image/media.py

Lines changed: 83 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
11
#!/usr/bin/env python3
22
import argparse
3+
import sys
4+
import io
35
from pathlib import Path
46
from PIL import Image
57
import cairosvg
68

7-
# Take build directory as argument to save generated C files and PNG files.
89
parser = argparse.ArgumentParser(description="Convert image files to C structs.")
9-
parser.add_argument("dest", help="Destination build folder for PNG files.")
10+
parser.add_argument("-o", "--output", type=Path, help="Output file name for C structs.")
11+
parser.add_argument(
12+
"--decls", type=Path, help="Output file name for struct declarations."
13+
)
14+
parser.add_argument(
15+
"--decls-include",
16+
help="Struct declaration include file name, generated from output file if given.",
17+
)
18+
parser.add_argument("--attrs", type=Path, help="Output file name for attribute list.")
19+
parser.add_argument("images", type=Path, nargs="+", help="Input file name(s).")
1020
args = parser.parse_args()
1121

12-
build_dir = Path(args.dest)
13-
build_dir.mkdir(parents=True, exist_ok=True)
14-
media_dir = Path(__file__).parent / "media"
15-
16-
# Convert all SVG files in media_dir to PNG and save in build_dir if not already present.
17-
svg_files = media_dir.rglob("*.svg")
18-
for svg in svg_files:
19-
png = svg.with_suffix(".png").name
20-
png_path = build_dir / png
21-
if png_path.exists() and png_path.stat().st_mtime >= svg.stat().st_mtime:
22-
continue
23-
with open(svg, "rb") as svg_file:
24-
png_bytes = cairosvg.svg2png(file_obj=svg_file)
25-
with open(png_path, "wb") as out_png:
26-
out_png.write(png_bytes)
27-
28-
# Collect all image files in media_dir (png, bmp, jpg) and build_dir (png), including subfolders.
29-
media_images = (
30-
list(media_dir.rglob("*.png"))
31-
+ list(media_dir.rglob("*.bmp"))
32-
+ list(media_dir.rglob("*.jpg"))
33-
+ list(build_dir.rglob("*.png"))
34-
)
22+
if args.attrs:
23+
if args.decls_include is not None:
24+
decls_include = args.decls_include
25+
elif args.decls is not None:
26+
decls_include = args.decls.name
27+
else:
28+
parser.error("Need struct declaration include file name.")
29+
30+
31+
def load_image(path):
32+
if path.suffix == ".svg":
33+
with open(path, "rb") as f:
34+
png_bytes = cairosvg.svg2png(file_obj=f)
35+
return Image.open(io.BytesIO(png_bytes))
36+
else:
37+
return Image.open(path)
3538

3639

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

6265
# Process each image.
6366
results = {}
64-
for img_path in media_images:
65-
with Image.open(img_path) as img:
66-
name = Path(img_path.name).stem
67-
width, height, bin_data = image_to_8bit_map(img)
68-
results[name] = (width, height, bin_data)
67+
for img_path in args.images:
68+
name = img_path.stem
69+
if args.output:
70+
with load_image(img_path) as img:
71+
width, height, bin_data = image_to_8bit_map(img)
72+
results[name] = (width, height, bin_data)
73+
else:
74+
results[name] = (0, 0, None)
6975

7076

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

78-
# Parse bytes for printing.
79-
bytes_per_line = 12
80-
lines = []
81-
for i in range(0, len(bin_data), bytes_per_line):
82-
chunk = bin_data[i : i + bytes_per_line]
83-
line = " " + ", ".join(f"0x{val:02x}" for val in chunk)
84-
lines.append(line)
85-
data_literal = ",\n".join(lines) + ","
86-
87-
# Printed C structs.
88-
structs += f"static const uint8_t {name}_data[] = {{\n{data_literal}\n}};\n\n"
89-
structs += (
90-
f"const pbio_image_monochrome_t pbio_image_media_{name} = {{\n"
91-
f" .width = {width},\n"
92-
f" .height = {height},\n"
93-
f" .data = {name}_data,\n"
94-
f"}};\n"
95-
)
84+
if args.output:
85+
# Parse bytes for printing.
86+
bytes_per_line = 12
87+
lines = []
88+
for i in range(0, len(bin_data), bytes_per_line):
89+
chunk = bin_data[i : i + bytes_per_line]
90+
line = " " + ", ".join(f"0x{val:02x}" for val in chunk)
91+
lines.append(line)
92+
data_literal = ",\n".join(lines) + ","
93+
94+
# Printed C structs.
95+
structs += f"static const uint8_t {name}_data[] = {{\n{data_literal}\n}};\n\n"
96+
structs += (
97+
f"const pbio_image_monochrome_t pbio_image_media_{name} = {{\n"
98+
f" .width = {width},\n"
99+
f" .height = {height},\n"
100+
f" .data = {name}_data,\n"
101+
f"}};\n"
102+
)
96103

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

101108

102-
HEADER = """// SPDX-License-Identifier: MIT
103-
//Copyright (c) 2025 The Pybricks Authors
109+
HEADER = """// Generated file, edit with care.
104110
105111
#include <pbio/image.h>
106112
"""
107113

108-
with open(build_dir / "pbio_image_media.c", "w") as f:
109-
f.write(HEADER)
110-
f.write('#include "pbio_image_media.h"\n\n')
111-
f.write(structs)
112-
113-
with open(build_dir / "pbio_image_media.h", "w") as f:
114-
f.write(HEADER)
115-
f.write("#ifndef _PBIO_IMAGE_MEDIA_H_\n")
116-
f.write("#define _PBIO_IMAGE_MEDIA_H_\n\n")
117-
f.write(externs)
118-
f.write("#endif // _PBIO_IMAGE_MEDIA_H_\n")
119-
120-
with open(build_dir / "pb_type_image_attributes.c", "w") as f:
121-
f.write(HEADER)
122-
f.write('#include "pbio_image_media.h"\n\n')
123-
f.write("#include <py/obj.h>\n\n")
124-
f.write(
125-
"static const mp_rom_map_elem_t pb_type_image_attributes_dict_table[] = {\n"
126-
)
127-
f.write(qstrtab)
128-
f.write("};\n")
129-
f.write(
130-
"MP_DEFINE_CONST_DICT(pb_type_image_attributes_dict, pb_type_image_attributes_dict_table);"
131-
)
114+
if args.output:
115+
with open(args.output, "w") as f:
116+
f.write(HEADER)
117+
f.write(structs)
118+
119+
if args.decls:
120+
guard = args.decls.name.upper().replace(".", "_")
121+
with open(args.decls, "w") as f:
122+
f.write(HEADER)
123+
f.write(f"#ifndef _{guard}_\n")
124+
f.write(f"#define _{guard}_\n\n")
125+
f.write(externs)
126+
f.write(f"#endif // _{guard}_\n")
127+
128+
if args.attrs:
129+
with open(args.attrs, "w") as f:
130+
f.write(HEADER)
131+
f.write(f'#include "{decls_include}"\n\n')
132+
f.write("#include <py/obj.h>\n\n")
133+
f.write(
134+
"static const mp_rom_map_elem_t pb_type_image_attributes_dict_table[] = {\n"
135+
)
136+
f.write(qstrtab)
137+
f.write("};\n")
138+
f.write(
139+
"MP_DEFINE_CONST_DICT(pb_type_image_attributes_dict, pb_type_image_attributes_dict_table);"
140+
)

0 commit comments

Comments
 (0)