Skip to content

Commit 54dce17

Browse files
committed
pbio/image/media: Add mechanism for images.
Include images in the firmware, which is needed for the UI. Also provides access to images via `ImageFile` class.
1 parent c31d077 commit 54dce17

9 files changed

Lines changed: 513 additions & 0 deletions

File tree

bricks/_common/common.mk

Lines changed: 12 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,10 +550,21 @@ ifneq ($(PB_MCU_FAMILY),TIAM1808)
549550
SRC_S += lib/pbio/platform/$(PBIO_PLATFORM)/startup.s
550551
endif
551552

553+
ifeq ($(PB_LIB_UMM_MALLOC),1)
554+
MEDIA = $(BUILD)/pbio_image_media.c
555+
else
556+
MEDIA :=
557+
endif
558+
559+
$(BUILD)/pbio_image_media.c: $(MEDIA_CONVERT)
560+
$(ECHO) "MEDIA generating image media files"
561+
$(Q)$(PYTHON) $(MEDIA_CONVERT) $(BUILD)
562+
552563
OBJ = $(PY_O)
553564
OBJ += $(addprefix $(BUILD)/, $(SRC_S:.s=.o))
554565
OBJ += $(addprefix $(BUILD)/, $(PY_EXTRA_SRC_C:.c=.o))
555566
OBJ += $(addprefix $(BUILD)/, $(PYBRICKS_PYBRICKS_SRC_C:.c=.o))
567+
OBJ += $(BUILD)/pbio_image_media.o
556568

557569
OBJ += $(addprefix $(BUILD)/, $(LWRB_SRC_C:.c=.o))
558570
OBJ += $(addprefix $(BUILD)/, $(PBIO_SRC_C:.c=.o))

lib/pbio/include/pbio/image.h

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

72+
/**
73+
* Compression types.
74+
*/
75+
typedef enum {
76+
/**
77+
* One byte per pixel, same as uncompressed image.
78+
*/
79+
PBIO_IMAGE_COMPRESSION_TYPE_NONE,
80+
/**
81+
* One byte for 8 pixels. High is black, most significant bit is first pixel.
82+
*/
83+
PBIO_IMAGE_COMPRESSION_MONOCHROME_8BIT_MAP,
84+
/**
85+
* Monochrome image limited to 256x256 pixels.
86+
*
87+
* Image starts at black. Data is an array of 2x8-bit values
88+
* representing the coordinates where the color inverts.
89+
*/
90+
PBIO_IMAGE_COMPRESSION_MONOCHROME_256x256_FLIP,
91+
} pbio_image_compression_type_t;
92+
93+
/**
94+
* Compressed image.
95+
*/
96+
typedef struct _pbio_image_compressed_t {
97+
/**
98+
* Compression type.
99+
*/
100+
pbio_image_compression_type_t type;
101+
/**
102+
* Width of the image, or number of columns.
103+
*/
104+
int width;
105+
/**
106+
* Height of the image, or number of rows.
107+
*/
108+
int height;
109+
/**
110+
* Compressed data in the format for the given compression type.
111+
*/
112+
const uint8_t *data;
113+
/**
114+
* Data size.
115+
*/
116+
size_t data_size;
117+
/**
118+
* Name for lookup from user space.
119+
*/
120+
const char *name;
121+
} pbio_image_compressed_t;
122+
72123
/**
73124
* Coordinates of a rectangle.
74125
*/
@@ -107,6 +158,9 @@ void pbio_image_draw_image(pbio_image_t *image, const pbio_image_t *source,
107158
void pbio_image_draw_image_transparent(pbio_image_t *image,
108159
const pbio_image_t *source, int x, int y, uint8_t value);
109160

161+
void pbio_image_draw_image_from_compressed(pbio_image_t *image,
162+
const pbio_image_compressed_t *compressed, uint8_t value);
163+
110164
void pbio_image_draw_pixel(pbio_image_t *image, int x, int y, uint8_t value);
111165

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

lib/pbio/src/image/image.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#if PBIO_CONFIG_IMAGE
77

88
#include <pbio/image.h>
9+
#include <pbio/util.h>
910

1011
#include <string.h>
1112
#include <limits.h>
@@ -196,6 +197,58 @@ void pbio_image_draw_image_transparent(pbio_image_t *image,
196197
}
197198
}
198199

200+
/**
201+
* Initializes an image from a compressed image source.
202+
*
203+
* @param [in] image Destination image to draw into.
204+
* @param [in] compressed The compressed image source.
205+
* @param [in] value Value of high pixels.
206+
*
207+
* Source image pixels are copied into destination image and size is adopted.
208+
*/
209+
void pbio_image_draw_image_from_compressed(pbio_image_t *image, const pbio_image_compressed_t *compressed, uint8_t value) {
210+
211+
int w = compressed->width;
212+
int h = compressed->height;
213+
214+
image->width = w;
215+
image->stride = w;
216+
image->height = h;
217+
image->print_font = NULL;
218+
image->print_x_left = 0;
219+
image->print_y_top = 0;
220+
image->print_value = 0;
221+
222+
switch (compressed->type) {
223+
case PBIO_IMAGE_COMPRESSION_MONOCHROME_256x256_FLIP: {
224+
// Unpack strokes of one color up to next transition.
225+
uint8_t value_now = 0;
226+
uint16_t pos_last = 0;
227+
for (size_t i = 0; i <= compressed->data_size; i += 2) {
228+
uint16_t pos_next = i == compressed->data_size ?
229+
w * h : // End of image.
230+
pbio_get_uint16_le(&compressed->data[i]);
231+
memset(&image->pixels[pos_last], value_now, pos_next - pos_last);
232+
pos_last = pos_next;
233+
value_now = value_now ? 0 : value;
234+
}
235+
break;
236+
}
237+
case PBIO_IMAGE_COMPRESSION_MONOCHROME_8BIT_MAP: {
238+
// Unpack bits to bytes.
239+
for (int y = 0; y < image->height; y++) {
240+
for (int x = 0; x < image->width; x++) {
241+
size_t index = y * w + x;
242+
image->pixels[index] = compressed->data[index / 8] & (1 << (7 - index % 8)) ? value : 0;
243+
}
244+
}
245+
break;
246+
}
247+
default:
248+
break;
249+
}
250+
}
251+
199252
/**
200253
* Draw a single pixel.
201254
* @param [in] image Image to draw into.

lib/pbio/src/image/media.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import subprocess
4+
from pathlib import Path
5+
from PIL import Image
6+
import struct
7+
8+
# Take build directory as argument to save generated C files and PNG files.
9+
parser = argparse.ArgumentParser(description="Convert SVG files to PNG.")
10+
parser.add_argument("dest", help="Destination build folder for PNG files.")
11+
args = parser.parse_args()
12+
13+
build_dir = Path(args.dest)
14+
build_dir.mkdir(parents=True, exist_ok=True)
15+
media_dir = Path(__file__).parent / "media"
16+
17+
# Convert all SVG files in media_dir to PNG and save in build_dir if not already present.
18+
svg_files = media_dir.rglob("*.svg")
19+
for svg in svg_files:
20+
png = svg.with_suffix(".png").name
21+
png_path = build_dir / png
22+
if png_path.exists():
23+
continue
24+
subprocess.run(["cairosvg", str(svg), "-o", str(png_path)])
25+
26+
# Collect all image files in media_dir (png, bmp, jpg) and build_dir (png), including subfolders.
27+
media_images = (
28+
list(media_dir.rglob("*.png"))
29+
+ list(media_dir.rglob("*.bmp"))
30+
+ list(media_dir.rglob("*.jpg"))
31+
+ list(build_dir.rglob("*.png"))
32+
)
33+
34+
35+
# Convert rgba to monochrome, treating fully transparent pixels as white.
36+
def is_black(r, g, b, a):
37+
if a == 0:
38+
return 0
39+
return 1 if (r + g + b) < (128 * 3) else 0
40+
41+
42+
def image_to_mono_flip_256(img):
43+
img = img.convert("RGBA")
44+
pixels = img.load()
45+
46+
# Flatten the image into a single list in raster order
47+
mono = [is_black(*pixels[x, y]) for y in range(height) for x in range(width)]
48+
last = 0
49+
flips = []
50+
51+
if width > 256 or height > 256:
52+
raise ValueError("Image is too large for flip compression.")
53+
54+
for idx in range(width * height):
55+
if mono[idx] != last:
56+
flips.append(idx)
57+
last = mono[idx]
58+
59+
# Pack flip indices into bytes, 2 indices per byte
60+
return struct.pack(f"<{len(flips)}h", *flips)
61+
62+
63+
def image_to_8bit_map(img):
64+
img = img.convert("RGBA")
65+
width, height = img.size
66+
pixels = img.load()
67+
mono = [is_black(*pixels[x, y]) for y in range(height) for x in range(width)]
68+
69+
# go in chunks of 8 pixels and pack into a byte
70+
data = []
71+
for i in range(0, len(mono), 8):
72+
byte = 0
73+
for j in range(8):
74+
if i + j < len(mono):
75+
byte |= mono[i + j] << (7 - j)
76+
data.append(byte)
77+
78+
return bytes(data)
79+
80+
81+
COMPRESSION_TYPES = {
82+
"PBIO_IMAGE_COMPRESSION_MONOCHROME_8BIT_MAP": image_to_8bit_map,
83+
"PBIO_IMAGE_COMPRESSION_MONOCHROME_256x256_FLIP": image_to_mono_flip_256,
84+
}
85+
86+
87+
# Get printable C struct for the compressed image data.
88+
def get_c_const_struct(name, compression_type, width, height, flip_data):
89+
bytes_per_line = 12
90+
lines = []
91+
for i in range(0, len(flip_data), bytes_per_line):
92+
chunk = flip_data[i : i + bytes_per_line]
93+
line = " " + ", ".join(f"0x{val:02x}" for val in chunk)
94+
lines.append(line)
95+
data_literal = ",\n".join(lines) + ","
96+
97+
return f"""
98+
static const uint8_t {name}_data[] = {{
99+
{data_literal}
100+
}};
101+
102+
const pbio_image_compressed_t pbio_image_media_{name} = {{
103+
.type = {compression_type},
104+
.width = {width},
105+
.height = {height},
106+
.name = "{name.upper()}",
107+
.data = {name}_data,
108+
.data_size = sizeof({name}_data),
109+
}};
110+
"""
111+
112+
113+
c_file_contents = """// SPDX-License-Identifier: MIT
114+
// Copyright (c) 2025 The Pybricks Authors
115+
116+
#include <pbio/image.h>
117+
#include <pbio/util.h>
118+
#include <string.h>
119+
"""
120+
121+
c_struct_names = []
122+
123+
h_file_contents = f"{c_file_contents}\n\nextern const pbio_image_compressed_t *pbio_image_media_lookup(const char *name);\n"
124+
125+
126+
# Process each image. Compress using both methods and choose the smaller one.
127+
for img_path in media_images:
128+
with Image.open(img_path) as img:
129+
name = Path(img_path.name).stem
130+
131+
width, height = img.size
132+
133+
min_size = width * height
134+
print(name)
135+
for compression_type, compression_func in COMPRESSION_TYPES.items():
136+
bin_data = compression_func(img)
137+
print(f" {compression_type}: {len(bin_data)} bytes")
138+
if len(bin_data) < min_size:
139+
min_size = len(bin_data)
140+
best_compression_type = compression_type
141+
best_bin_data = bin_data
142+
143+
if len(best_bin_data) >= width * height:
144+
raise ValueError(f"Warning: {name} is not smaller than raw bitmap.")
145+
146+
c_struct_names.append("pbio_image_media_" + name)
147+
c_file_contents += get_c_const_struct(
148+
name, best_compression_type, width, height, best_bin_data
149+
)
150+
h_file_contents += (
151+
f"\nextern const pbio_image_compressed_t pbio_image_media_{name};\n"
152+
)
153+
154+
155+
c_file_contents += "\nstatic const pbio_image_compressed_t pbio_image_media_all[] = {\n"
156+
for struct_name in c_struct_names:
157+
c_file_contents += f" {struct_name},\n"
158+
c_file_contents += "};\n"
159+
160+
c_file_contents += """
161+
const pbio_image_compressed_t *pbio_image_media_lookup(const char *name) {
162+
for (size_t i = 0; i < PBIO_ARRAY_SIZE(pbio_image_media_all); i++) {
163+
if (strncmp(pbio_image_media_all[i].name, name, strlen(pbio_image_media_all[i].name)) == 0) {
164+
return &pbio_image_media_all[i];
165+
}
166+
}
167+
return NULL;
168+
}
169+
"""
170+
171+
with open(build_dir / "pbio_image_media.c", "w") as c_file:
172+
c_file.write(c_file_contents)
173+
174+
with open(build_dir / "pbio_image_media.h", "w") as h_file:
175+
h_file.write(h_file_contents)
539 Bytes
Loading

0 commit comments

Comments
 (0)