diff --git a/Documentation/conf.py b/Documentation/conf.py index a09ee94ed0c473d97e1e1f216f6338df00294105..3722ebea58b6e2b2d3d21bca019eed18f2b289fc 100644 --- a/Documentation/conf.py +++ b/Documentation/conf.py @@ -124,6 +124,7 @@ autodoc_mock_imports = [ "sys_leds", "sys_max30001", "sys_max86150", + "sys_png", "sys_config", "ucollections", "uerrno", diff --git a/Documentation/index.rst b/Documentation/index.rst index 2816dcd17ed6f00868c18b0b8b6ef83c21c9cf9f..7df2a217e5f770971aeea3a4f03fcbf363a386a5 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -36,6 +36,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the pycardium/light-sensor pycardium/os pycardium/personal_state + pycardium/png pycardium/power pycardium/pride pycardium/simple_menu diff --git a/Documentation/pycardium/png.rst b/Documentation/pycardium/png.rst new file mode 100644 index 0000000000000000000000000000000000000000..d4a4aa41c4a9860e77da4b1ae137c49c779e93fd --- /dev/null +++ b/Documentation/pycardium/png.rst @@ -0,0 +1,7 @@ +``png`` - PNG Decoder +=============== +The ``png`` module provides functions to decode PNG files into raw pixel data +which can be displayed using the card10's display or its LEDs. + +.. automodule:: png + :members: diff --git a/pycardium/meson.build b/pycardium/meson.build index 8621e66128ca22cecdd3d53b021f5ee03d1fab1d..38d5521e1989983fead5aeca90a42101e81b4482 100644 --- a/pycardium/meson.build +++ b/pycardium/meson.build @@ -21,6 +21,7 @@ modsrc = files( 'modules/sys_bme680.c', 'modules/sys_display.c', 'modules/sys_leds.c', + 'modules/sys_png.c', 'modules/utime.c', 'modules/vibra.c', 'modules/ws2812.c', @@ -99,7 +100,7 @@ elf = executable( mp_headers, version_hdr, include_directories: micropython_includes, - dependencies: [max32665_startup_core1, periphdriver, rd117, api_caller], + dependencies: [max32665_startup_core1, periphdriver, rd117, api_caller, lodepng], link_with: upy, link_whole: [max32665_startup_core1_lib, api_caller_lib], link_args: [ diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build index 0130c75b8407fb77535b33bff087ab6160c3b5ff..6f0fccbb0dfd0339bc5ecd761dc0668562e7ea1c 100644 --- a/pycardium/modules/py/meson.build +++ b/pycardium/modules/py/meson.build @@ -10,6 +10,7 @@ python_modules = files( 'leds.py', 'max30001.py', 'max86150.py', + 'png.py', 'pride.py', 'simple_menu.py', diff --git a/pycardium/modules/py/png.py b/pycardium/modules/py/png.py new file mode 100644 index 0000000000000000000000000000000000000000..63fa2e1a75b31227358855349e9f1ed944ad8010 --- /dev/null +++ b/pycardium/modules/py/png.py @@ -0,0 +1,76 @@ +import sys_png +import color + + +def decode(png_data, format="RGB", bg=color.BLACK): + """ + Decode a PNG image and return raw pixel data. + + :param str format: The intended output format: + + - ``RGB``: 24 bit RGB. + - ``RGBA``: 24 bit RGB + 8 bit alpha. + - ``565``: 16 bit RGB. This consumes 1 byte less RAM per pixel than ``RGB``. + - ``565A``: 16 bit RGB + 8 bit alpha. + + Default is ``RGB``. + + :param Color bg: Background color. + + If the PNG contains an alpha channel but no alpha + channel is requested in the output (``RGB`` or ``565``) + this color will be used as the background color. + + Default is ``Color.BLACK``. + + :returns: Typle ``(width, height, data)`` + + .. versionadded:: 1.17 + + **Example with RGB data:** + + .. code-block:: python + + import display + import png + + # Draw a PNG file to the display + f = open("example.png") + w, h, img = png.decode(f.read()) + f.close() + with display.open() as d: + d.clear() + d.blit(0, 0, w, h, img) + d.update() + + + **Example with rgb565 data:** + + .. code-block:: python + + import display + import png + + # Draw a PNG file to the display + f = open("example.png") + w, h, img = png.decode(f.read(), "565") + f.close() + with display.open() as d: + d.clear() + d.blit(0, 0, w, h, img, True) + d.update() + + """ + + formats = ("RGB", "RGBA", "565", "565A") + if format not in formats: + raise ValueError("Supported formats: " + ",".join(formats)) + + if format == "RGB": + return sys_png.decode(png_data, 0, 0, bg) + if format == "RGBA": + return sys_png.decode(png_data, 0, 1, bg) + if format == "565": + return sys_png.decode(png_data, 1, 0, bg) + if format == "565A": + return sys_png.decode(png_data, 1, 1, bg) diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h index 58f4f80d59d9b8ab97b9687a0dcf462f701b81a6..ad36658088e1d9c89126d7247acdfd439cb24ae8 100644 --- a/pycardium/modules/qstrdefs.h +++ b/pycardium/modules/qstrdefs.h @@ -220,3 +220,7 @@ Q(send_report) /* SpO2 */ Q(spo2_algo) Q(maxim_rd117) + +/* PNG */ +Q(sys_png) +Q(decode) diff --git a/pycardium/modules/sys_png.c b/pycardium/modules/sys_png.c new file mode 100644 index 0000000000000000000000000000000000000000..78c819f496b89058da7a287276927286b8f58d00 --- /dev/null +++ b/pycardium/modules/sys_png.c @@ -0,0 +1,183 @@ +#include "epicardium.h" + +#define LODEPNG_NO_COMPILE_ENCODER +#define LODEPNG_NO_COMPILE_DISK +#define LODEPNG_NO_COMPILE_ALLOCATORS +#include "lodepng.h" + +#include "py/builtin.h" +#include "py/binary.h" +#include "py/obj.h" +#include "py/objarray.h" +#include "py/runtime.h" +#include "py/gc.h" + +void *lodepng_malloc(size_t size) +{ + return m_malloc(size); +} + +void *lodepng_realloc(void *ptr, size_t new_size) +{ + return m_realloc(ptr, new_size); +} + +void lodepng_free(void *ptr) +{ + m_free(ptr); +} + +static void lode_raise(unsigned int status) +{ + if (status) { + nlr_raise(mp_obj_new_exception_msg_varg( + &mp_type_ValueError, lodepng_error_text(status)) + ); + } +} + +static inline uint16_t rgb888_to_rgb565(uint8_t *bytes) +{ + return ((bytes[0] & 0b11111000) << 8) | ((bytes[1] & 0b11111100) << 3) | + (bytes[2] >> 3); +} + +static inline uint8_t apply_alpha(uint8_t in, uint8_t bg, uint8_t alpha) +{ + /* Not sure if it is worth (or even a good idea) to have + * the special cases here. */ + if (bg == 0) { + return (in * alpha) / 255; + } + + uint8_t beta = 255 - alpha; + + if (bg == 255) { + return ((in * alpha) / 255) + beta; + } + + return (in * alpha + bg * beta) / 255; +} + +static mp_obj_t mp_png_decode(size_t n_args, const mp_obj_t *args) +{ + mp_buffer_info_t png_info; + + mp_obj_t png = args[0]; + mp_obj_t rgb565_out = args[1]; + mp_obj_t alpha_out = args[2]; + mp_obj_t bg = args[3]; + + /* Load buffer and ensure it contains enough data */ + if (!mp_get_buffer(png, &png_info, MP_BUFFER_READ)) { + mp_raise_TypeError("png does not support buffer protocol."); + } + + if (mp_obj_get_int(mp_obj_len(bg)) < 3) { + mp_raise_ValueError("bg must have 3 elements."); + } + + int i, j; + + unsigned int w, h; + uint8_t *raw; + int raw_len; + int raw_len_original; + + LodePNGState state; + lodepng_state_init(&state); + state.decoder.ignore_crc = 1; + lode_raise(lodepng_inspect(&w, &h, &state, png_info.buf, png_info.len)); + + unsigned alpha_in = lodepng_can_have_alpha(&(state.info_png.color)); + + /* Do we need to consider an alpha channel? */ + if (alpha_in || mp_obj_is_true(alpha_out)) { + lode_raise(lodepng_decode32( + &raw, &w, &h, png_info.buf, png_info.len) + ); + raw_len = w * h * 4; + } else { + lode_raise(lodepng_decode24( + &raw, &w, &h, png_info.buf, png_info.len) + ); + raw_len = w * h * 4; + } + + raw_len_original = raw_len; + + /* User did not ask for alpha, but input might contain alpha. + * Remove alpha using provided background color. */ + if (alpha_in && !mp_obj_is_true(alpha_out)) { + uint8_t bg_red = mp_obj_get_int( + mp_obj_subscr(bg, mp_obj_new_int(0), MP_OBJ_SENTINEL) + ); + uint8_t bg_green = mp_obj_get_int( + mp_obj_subscr(bg, mp_obj_new_int(1), MP_OBJ_SENTINEL) + ); + uint8_t bg_blue = mp_obj_get_int( + mp_obj_subscr(bg, mp_obj_new_int(2), MP_OBJ_SENTINEL) + ); + + for (i = 0, j = 0; i < raw_len; i += 4, j += 3) { + uint8_t alpha = raw[i + 3]; + raw[j] = apply_alpha(raw[i], bg_red, alpha); + raw[j + 1] = apply_alpha(raw[i + 1], bg_green, alpha); + raw[j + 2] = apply_alpha(raw[i + 2], bg_blue, alpha); + } + raw_len = w * h * 3; + } + + if (mp_obj_is_true(rgb565_out)) { + if (mp_obj_is_true(alpha_out)) { + for (i = 0, j = 0; i < raw_len; i += 4, j += 3) { + uint16_t c = rgb888_to_rgb565(&raw[i]); + raw[j] = c & 0xFF; + raw[j + 1] = c >> 8; + raw[j + 2] = raw[i + 3]; + } + raw_len = w * h * 3; + } else { + for (i = 0, j = 0; i < raw_len; i += 3, j += 2) { + uint16_t c = rgb888_to_rgb565(&raw[i]); + raw[j] = c & 0xFF; + raw[j + 1] = c >> 8; + } + raw_len = w * h * 2; + } + } + + if (raw_len != raw_len_original) { + /* Some conversion shrank the buffer. + * Reallocate to free the unneeded RAM. */ + m_realloc(raw, raw_len); + } + + mp_obj_t mp_w = MP_OBJ_NEW_SMALL_INT(w); + mp_obj_t mp_h = MP_OBJ_NEW_SMALL_INT(h); + mp_obj_t mp_raw = mp_obj_new_memoryview( + MP_OBJ_ARRAY_TYPECODE_FLAG_RW | BYTEARRAY_TYPECODE, + raw_len, + raw + ); + mp_obj_t tup[] = { mp_w, mp_h, mp_raw }; + + return mp_obj_new_tuple(3, tup); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(decode, 4, 4, mp_png_decode); + +static const mp_rom_map_elem_t png_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_png) }, + { MP_ROM_QSTR(MP_QSTR_decode), MP_ROM_PTR(&decode) }, +}; +static MP_DEFINE_CONST_DICT(png_module_globals, png_module_globals_table); + +// Define module object. +const mp_obj_module_t png_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&png_module_globals, +}; + +/* Register the module to make it available in Python */ +/* clang-format off */ +MP_REGISTER_MODULE(MP_QSTR_sys_png, png_module, MODULE_PNG_ENABLED); diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h index 7e2a56ec31356d50f45d5511560a8188daa19a87..fbe33070bc8425a91f7a12cfe6ca35a16ea71a0f 100644 --- a/pycardium/mpconfigport.h +++ b/pycardium/mpconfigport.h @@ -78,6 +78,7 @@ int mp_hal_csprng_read_int(void); #define MODULE_WS2812_ENABLED (1) #define MODULE_CONFIG_ENABLED (1) #define MODULE_BLE_ENABLED (1) +#define MODULE_PNG_ENABLED (1) #define MICROPY_BLUETOOTH_CARD10 (1) /*