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
10 changes: 10 additions & 0 deletions bricks/_common/common.mk
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ DFU = $(TOP)/tools/dfu.py
PYDFU = $(TOP)/tools/pydfu.py
PYBRICKSDEV = pybricksdev
METADATA = $(PBTOP)/tools/metadata.py
MEDIA_CONVERT = $(PBTOP)/lib/pbio/src/image/media.py
OPENOCD ?= openocd
OPENOCD_CONFIG ?= openocd_stm32$(PB_MCU_SERIES_LCASE).cfg
TEXT0_ADDR ?= 0x08000000
Expand Down Expand Up @@ -549,6 +550,11 @@ ifneq ($(PB_MCU_FAMILY),TIAM1808)
SRC_S += lib/pbio/platform/$(PBIO_PLATFORM)/startup.s
endif

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

OBJ = $(PY_O)
OBJ += $(addprefix $(BUILD)/, $(SRC_S:.s=.o))
OBJ += $(addprefix $(BUILD)/, $(PY_EXTRA_SRC_C:.c=.o))
Expand Down Expand Up @@ -664,6 +670,10 @@ else
FW_SECTIONS :=
endif

$(BUILD)/pbio_image_media.c $(BUILD)/pb_type_image_attributes.c: $(MEDIA_CONVERT)
$(ECHO) "MEDIA generating image media files"
$(Q)$(PYTHON) $(MEDIA_CONVERT) $(BUILD)

$(BUILD)/firmware.elf: $(LD_FILES) $(OBJ)
$(ECHO) "LINK $@"
$(Q)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJ) $(LIBS)
Expand Down
1 change: 1 addition & 0 deletions bricks/ev3/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ PB_MCU_FAMILY = TIAM1808

PB_LIB_UMM_MALLOC = 1
PB_LIB_BTSTACK = 1
PB_MEDIA = 1

include ../_common/common.mk
1 change: 1 addition & 0 deletions bricks/ev3/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
#define PYBRICKS_PY_PARAMETERS_ICON (1)
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (1)
#define PYBRICKS_PY_DEVICES (1)
#define PYBRICKS_PY_PUPDEVICES (0)
#define PYBRICKS_PY_PUPDEVICES_REMOTE (0)
Expand Down
1 change: 1 addition & 0 deletions bricks/nxt/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
#define PYBRICKS_PY_PARAMETERS_ICON (0)
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (0)
#define PYBRICKS_PY_DEVICES (1)
#define PYBRICKS_PY_ROBOTICS (1)
#define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0)
Expand Down
1 change: 1 addition & 0 deletions bricks/virtualhub/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ PB_FROZEN_MODULES = 1
MICROPY_ROM_TEXT_COMPRESSION = 1
PB_LIB_UMM_MALLOC = 1
PB_LIB_BTSTACK = 1
PB_MEDIA = 1

include ../_common/common.mk
1 change: 1 addition & 0 deletions bricks/virtualhub/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#define PYBRICKS_PY_PARAMETERS_BUTTON (1)
#define PYBRICKS_PY_PARAMETERS_ICON (0)
#define PYBRICKS_PY_PARAMETERS_IMAGE (1)
#define PYBRICKS_PY_PARAMETERS_IMAGE_FILE (1)
#define PYBRICKS_PY_PUPDEVICES (1)
#define PYBRICKS_PY_PUPDEVICES_REMOTE (1)
#define PYBRICKS_PY_DEVICES (1)
Expand Down
22 changes: 22 additions & 0 deletions lib/pbio/include/pbio/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ typedef struct _pbio_image_t {
uint8_t print_value;
} pbio_image_t;

/**
* Compressed monochrome image.
*/
typedef struct _pbio_image_monochrome_t {
/**
* Width of the image, or number of columns.
*/
int width;
/**
* Height of the image, or number of rows.
*/
int height;
/**
* One byte is 8 pixels. High is black, low is transparent. Most
* significant bit is first pixel.
*/
const uint8_t *data;
} pbio_image_monochrome_t;

/**
* Coordinates of a rectangle.
*/
Expand Down Expand Up @@ -107,6 +126,9 @@ void pbio_image_draw_image(pbio_image_t *image, const pbio_image_t *source,
void pbio_image_draw_image_transparent(pbio_image_t *image,
const pbio_image_t *source, int x, int y, uint8_t value);

void pbio_image_draw_image_transparent_from_monochrome(pbio_image_t *image,
const pbio_image_monochrome_t *source, int x, int y, uint8_t value);

void pbio_image_draw_pixel(pbio_image_t *image, int x, int y, uint8_t value);

void pbio_image_draw_hline(pbio_image_t *image, int x, int y, int l,
Expand Down
47 changes: 47 additions & 0 deletions lib/pbio/src/image/image.c
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,53 @@ void pbio_image_draw_image_transparent(pbio_image_t *image,
}
}

/**
* Draw an image inside another image with transparency. The source is
* a compressed monochrome image.
*
* @param [in] image Destination image to draw into.
* @param [in] source Source image.
* @param [in] x X coordinate of the top-left point in destination
* image.
* @param [in] y Y coordinate of the top-left point in destination
* image.
* @param [in] value Pixel value in destination for black.
*
* Source image pixels are copied into destination image. When a source pixel
* matches the transparent value, the corresponding destination pixel is left
* untouched.
*
* Clipping: drawing is clipped to destination image dimensions.
*/
void pbio_image_draw_image_transparent_from_monochrome(pbio_image_t *image,
const pbio_image_monochrome_t *source, int x, int y, uint8_t value) {
// Clipping.
int ox = x;
int oy = y;
int x2 = x + source->width;
int y2 = y + source->height;
clip_or_return(x, x2, image->width);
clip_or_return(y, y2, image->height);

// Initial index in source.
size_t index = (y - oy) * source->width + (x - ox);

// Draw pixels.
uint8_t *dst = image->pixels + y * image->stride + x;
int w = x2 - x;
for (int h = y2 - y; h; h--) {
for (int i = w; i; i--) {
if (source->data[index / 8] & (1 << (7 - index % 8))) {
*dst = value;
}
index++;
dst++;
}
dst += image->stride - w;
index += source->width - w;
}
}

/**
* Draw a single pixel.
* @param [in] image Image to draw into.
Expand Down
131 changes: 131 additions & 0 deletions lib/pbio/src/image/media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import argparse
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 SVG files to PNG.")
parser.add_argument("dest", help="Destination build folder for PNG files.")
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"))
)


# Convert rgba to monochrome, treating fully transparent pixels as white.
def is_black(r, g, b, a):
if a == 0:
return 0
return 1 if (r + g + b) < (128 * 3) else 0


def image_to_8bit_map(img):
img = img.convert("RGBA")
width, height = img.size
pixels = img.load()
mono = [is_black(*pixels[x, y]) for y in range(height) for x in range(width)]

# go in chunks of 8 pixels and pack into a byte
data = []
for i in range(0, len(mono), 8):
byte = 0
for j in range(8):
if i + j < len(mono):
byte |= mono[i + j] << (7 - j)
data.append(byte)

return width, height, bytes(data)


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


externs = ""
structs = ""
qstrtab = ""

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

# 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

#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);"
)
Loading