Skip to content

Commit a6fd1a4

Browse files
committed
pbio/image/media: Add mechanism for images.
1 parent c31d077 commit a6fd1a4

15 files changed

Lines changed: 670 additions & 134 deletions

File tree

bricks/_common/common.mk

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ DFU = $(TOP)/tools/dfu.py
162162
PYDFU = $(TOP)/tools/pydfu.py
163163
PYBRICKSDEV = pybricksdev
164164
METADATA = $(PBTOP)/tools/metadata.py
165+
MEDIA_CONVERT = $(PBTOP)/lib/pbio/src/image/media.py
165166
OPENOCD ?= openocd
166167
OPENOCD_CONFIG ?= openocd_stm32$(PB_MCU_SERIES_LCASE).cfg
167168
TEXT0_ADDR ?= 0x08000000
@@ -549,6 +550,11 @@ ifneq ($(PB_MCU_FAMILY),TIAM1808)
549550
SRC_S += lib/pbio/platform/$(PBIO_PLATFORM)/startup.s
550551
endif
551552

553+
ifeq ($(PB_MEDIA),1)
554+
PYBRICKS_PYBRICKS_SRC_C += $(BUILD)/pb_type_image_attributes.c
555+
PBIO_SRC_C += $(BUILD)/pbio_image_media.c
556+
endif
557+
552558
OBJ = $(PY_O)
553559
OBJ += $(addprefix $(BUILD)/, $(SRC_S:.s=.o))
554560
OBJ += $(addprefix $(BUILD)/, $(PY_EXTRA_SRC_C:.c=.o))
@@ -664,6 +670,10 @@ else
664670
FW_SECTIONS :=
665671
endif
666672

673+
$(BUILD)/pbio_image_media.c $(BUILD)/pb_type_image_attributes.c: $(MEDIA_CONVERT)
674+
$(ECHO) "MEDIA generating image media files"
675+
$(Q)$(PYTHON) $(MEDIA_CONVERT) $(BUILD)
676+
667677
$(BUILD)/firmware.elf: $(LD_FILES) $(OBJ)
668678
$(ECHO) "LINK $@"
669679
$(Q)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJ) $(LIBS)

bricks/ev3/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ PB_MCU_FAMILY = TIAM1808
33

44
PB_LIB_UMM_MALLOC = 1
55
PB_LIB_BTSTACK = 1
6+
PB_MEDIA = 1
67

78
include ../_common/common.mk

bricks/ev3/mpconfigport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
5050
#define PYBRICKS_PY_PARAMETERS_ICON (1)
5151
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
52+
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (1)
5253
#define PYBRICKS_PY_DEVICES (1)
5354
#define PYBRICKS_PY_PUPDEVICES (0)
5455
#define PYBRICKS_PY_PUPDEVICES_REMOTE (0)

bricks/nxt/mpconfigport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
4646
#define PYBRICKS_PY_PARAMETERS_ICON (0)
4747
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
48+
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (0)
4849
#define PYBRICKS_PY_DEVICES (1)
4950
#define PYBRICKS_PY_ROBOTICS (1)
5051
#define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0)

bricks/virtualhub/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ PB_FROZEN_MODULES = 1
77
MICROPY_ROM_TEXT_COMPRESSION = 1
88
PB_LIB_UMM_MALLOC = 1
99
PB_LIB_BTSTACK = 1
10+
PB_MEDIA = 1
1011

1112
include ../_common/common.mk

bricks/virtualhub/mpconfigport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
4646
#define PYBRICKS_PY_PARAMETERS_ICON (0)
4747
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
48+
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (1)
4849
#define PYBRICKS_PY_PUPDEVICES (1)
4950
#define PYBRICKS_PY_PUPDEVICES_REMOTE (1)
5051
#define PYBRICKS_PY_DEVICES (1)

lib/pbio/include/pbio/image.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ typedef struct _pbio_image_t {
6969
uint8_t print_value;
7070
} pbio_image_t;
7171

72+
/**
73+
* Compressed monochrome image.
74+
*/
75+
typedef struct _pbio_image_monochrome_t {
76+
/**
77+
* Width of the image, or number of columns.
78+
*/
79+
int width;
80+
/**
81+
* Height of the image, or number of rows.
82+
*/
83+
int height;
84+
/**
85+
* One byte is 8 pixels. High is black, low is transparent. Most
86+
* significant bit is first pixel.
87+
*/
88+
const uint8_t *data;
89+
} pbio_image_monochrome_t;
90+
7291
/**
7392
* Coordinates of a rectangle.
7493
*/
@@ -107,6 +126,9 @@ void pbio_image_draw_image(pbio_image_t *image, const pbio_image_t *source,
107126
void pbio_image_draw_image_transparent(pbio_image_t *image,
108127
const pbio_image_t *source, int x, int y, uint8_t value);
109128

129+
void pbio_image_draw_image_transparent_from_monochrome(pbio_image_t *image,
130+
const pbio_image_monochrome_t *source, int x, int y, uint8_t value);
131+
110132
void pbio_image_draw_pixel(pbio_image_t *image, int x, int y, uint8_t value);
111133

112134
void pbio_image_draw_hline(pbio_image_t *image, int x, int y, int l,

lib/pbio/src/image/image.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,53 @@ void pbio_image_draw_image_transparent(pbio_image_t *image,
196196
}
197197
}
198198

199+
/**
200+
* Draw an image inside another image with transparency. The source is
201+
* a compressed monochrome image.
202+
*
203+
* @param [in] image Destination image to draw into.
204+
* @param [in] source Source image.
205+
* @param [in] x X coordinate of the top-left point in destination
206+
* image.
207+
* @param [in] y Y coordinate of the top-left point in destination
208+
* image.
209+
* @param [in] value Pixel value in destination for black.
210+
*
211+
* Source image pixels are copied into destination image. When a source pixel
212+
* matches the transparent value, the corresponding destination pixel is left
213+
* untouched.
214+
*
215+
* Clipping: drawing is clipped to destination image dimensions.
216+
*/
217+
void pbio_image_draw_image_transparent_from_monochrome(pbio_image_t *image,
218+
const pbio_image_monochrome_t *source, int x, int y, uint8_t value) {
219+
// Clipping.
220+
int ox = x;
221+
int oy = y;
222+
int x2 = x + source->width;
223+
int y2 = y + source->height;
224+
clip_or_return(x, x2, image->width);
225+
clip_or_return(y, y2, image->height);
226+
227+
// Initial index in source.
228+
size_t index = (y - oy) * source->width + (x - ox);
229+
230+
// Draw pixels.
231+
uint8_t *dst = image->pixels + y * image->stride + x;
232+
int w = x2 - x;
233+
for (int h = y2 - y; h; h--) {
234+
for (int i = w; i; i--) {
235+
if (source->data[index / 8] & (1 << (7 - index % 8))) {
236+
*dst = value;
237+
}
238+
index++;
239+
dst++;
240+
}
241+
dst += image->stride - w;
242+
index += source->width - w;
243+
}
244+
}
245+
199246
/**
200247
* Draw a single pixel.
201248
* @param [in] image Image to draw into.

lib/pbio/src/image/media.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
from pathlib import Path
4+
from PIL import Image
5+
import cairosvg
6+
7+
# Take build directory as argument to save generated C files and PNG files.
8+
parser = argparse.ArgumentParser(description="Convert SVG files to PNG.")
9+
parser.add_argument("dest", help="Destination build folder for PNG files.")
10+
args = parser.parse_args()
11+
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():
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+
)
35+
36+
37+
# Convert rgba to monochrome, treating fully transparent pixels as white.
38+
def is_black(r, g, b, a):
39+
if a == 0:
40+
return 0
41+
return 1 if (r + g + b) < (128 * 3) else 0
42+
43+
44+
def image_to_8bit_map(img):
45+
img = img.convert("RGBA")
46+
width, height = img.size
47+
pixels = img.load()
48+
mono = [is_black(*pixels[x, y]) for y in range(height) for x in range(width)]
49+
50+
# go in chunks of 8 pixels and pack into a byte
51+
data = []
52+
for i in range(0, len(mono), 8):
53+
byte = 0
54+
for j in range(8):
55+
if i + j < len(mono):
56+
byte |= mono[i + j] << (7 - j)
57+
data.append(byte)
58+
59+
return width, height, bytes(data)
60+
61+
62+
# Process each image.
63+
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)
69+
70+
71+
externs = ""
72+
structs = ""
73+
qstrtab = ""
74+
75+
for name in sorted(results):
76+
width, height, bin_data = results[name]
77+
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+
)
96+
97+
# Printed header and QSTR table entries.
98+
externs += f"extern const pbio_image_monochrome_t pbio_image_media_{name};\n\n"
99+
qstrtab += f" {{ MP_ROM_QSTR(MP_QSTR_{name.upper()}), MP_ROM_PTR(&pbio_image_media_{name}) }},\n"
100+
101+
102+
HEADER = """// SPDX-License-Identifier: MIT
103+
//Copyright (c) 2025 The Pybricks Authors
104+
105+
#include <pbio/image.h>
106+
"""
107+
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+
)

0 commit comments

Comments
 (0)