diff --git a/.gitmodules b/.gitmodules index a21dc049a14f22cdefc6a9cd09fc38c3a542404c..94ecfc21545420f03dd994fcaac512cbe85bac7a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/crypto/SHA256"] path = lib/crypto/SHA256 url = https://github.com/ilvn/SHA256 +[submodule "lib/lodepng/lodepng"] + path = lib/lodepng/lodepng + url = https://github.com/lvandeve/lodepng 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/bootloader/bootloader-display.c b/bootloader/bootloader-display.c index d04db551d6833aca9391cd5aad80139792ecfdf4..9b4b66fb4a9ca9de09028bea3aeccf49667a14e4 100644 --- a/bootloader/bootloader-display.c +++ b/bootloader/bootloader-display.c @@ -8,13 +8,12 @@ static void bootloader_display_splash(void) { - gfx_copy_region( + gfx_copy_region_rle_mono( &display_screen, 0, 0, 160, 80, - GFX_RLE_MONO, sizeof(splash), (const void *)(splash) ); diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h index 2cbe812e426020c21a25afe6c1b6043444a80a95..33edc9e61289f3dab3f1453c2ce371c55b0de773 100644 --- a/epicardium/epicardium.h +++ b/epicardium/epicardium.h @@ -1652,6 +1652,16 @@ enum disp_font_name { DISP_FONT24 = 4, }; +/* + * Image data type + */ +enum epic_rgb_format { + EPIC_RGB8 = 0, + EPIC_RGBA8 = 1, + EPIC_RGB565 = 2, + EPIC_RGBA5551 = 3, + }; + /** * Prints a string into the display framebuffer with font type selectable * @@ -1699,22 +1709,22 @@ API(API_DISP_PIXEL, int epic_disp_pixel( )); /** - * Blit an image buffer to display + * Blits an image buffer to the display * * :param x: x position * :param y: y position - * :param w: image width - * :param h: image height - * :param img: image data (rgb565) - * :param alpha: 8 bit alpha channel. Currently unused. + * :param w: Image width + * :param h: Image height + * :param img: Image data + * :param format: Format of the image data. One of :c:type:`epic_rgb_format`. */ API(API_DISP_BLIT, int epic_disp_blit( int16_t x, int16_t y, int16_t w, int16_t h, - uint16_t *img, - uint8_t *alpha + void *img, + enum epic_rgb_format format )); /** diff --git a/epicardium/main.c b/epicardium/main.c index 270f2b0a831765333f3cf0ea1acb0d5c497a106b..f4c7d9d94f3d135acc3d8de757e3143553829a72 100644 --- a/epicardium/main.c +++ b/epicardium/main.c @@ -48,14 +48,7 @@ int main(void) epic_disp_clear(0x0000); gfx_copy_region( - &display_screen, - 0, - 0, - 160, - 80, - GFX_RAW, - sizeof(version_splash), - version_splash + &display_screen, 0, 0, 160, 80, GFX_RGB565, version_splash ); if (strcmp(CARD10_VERSION, "v1.16") != 0) { diff --git a/epicardium/modules/display.c b/epicardium/modules/display.c index d7607a2027735db1813c86fc49c8a1718756d0b3..05b0eaec1d4a920a3e3f3dbfb3bf2054542371f7 100644 --- a/epicardium/modules/display.c +++ b/epicardium/modules/display.c @@ -92,61 +92,81 @@ int epic_disp_blit( int16_t pos_y, int16_t width, int16_t height, - uint16_t *img, - uint8_t *alpha + void *img, + enum epic_rgb_format format ) { - /* TODO: alpha is not supported yet */ int cl = check_lock(); if (cl < 0) { return cl; - } else { - int16_t offset_x = (pos_x < 0) ? -pos_x : 0; - int16_t count_x = width - offset_x; - int16_t offset_y = (pos_y < 0) ? -pos_y : 0; - int16_t count_y = height - offset_y; + } - if (pos_x + width >= 160) { - count_x -= (pos_x + width) % 160; - } - if (pos_y + height >= 80) { - count_y -= (pos_y + height) % 80; - } + int16_t offset_x = (pos_x < 0) ? -pos_x : 0; + int16_t count_x = width - offset_x; + int16_t offset_y = (pos_y < 0) ? -pos_y : 0; + int16_t count_y = height - offset_y; + + if (pos_x + width >= 160) { + count_x -= (pos_x + width) % 160; + } + if (pos_y + height >= 80) { + count_y -= (pos_y + height) % 80; + } + + size_t bpp; + enum gfx_encoding encoding; + + switch (format) { + case EPIC_RGB565: + bpp = 2; + encoding = GFX_RGB565; + break; + case EPIC_RGBA5551: + bpp = 2; + encoding = GFX_RGBA5551; + break; + case EPIC_RGB8: + bpp = 3; + encoding = GFX_RGB8; + break; + case EPIC_RGBA8: + bpp = 4; + encoding = GFX_RGBA8; + break; + default: + return -1; + break; + } - if (offset_x == 0 && offset_y == 0 && count_x == width && - count_y == height) { - /* Simply copy full image, no cropping or alpha blending */ + if (offset_x == 0 && offset_y == 0 && count_x == width && + count_y == height) { + /* Copy full image. No cropping.*/ + gfx_copy_region( + &display_screen, + pos_x, + pos_y, + width, + height, + encoding, + img + ); + } else { + /* Copy cropped image line by line. */ + int16_t curr_y; + for (curr_y = offset_y; curr_y < offset_y + count_y; curr_y++) { + uint8_t *line = img + (curr_y * width + offset_x) * bpp; gfx_copy_region( &display_screen, - pos_x, - pos_y, - width, - height, - GFX_RAW, - width * height * 2, - img + pos_x + offset_x, + pos_y + curr_y, + count_x, + 1, + encoding, + line ); - } else { - /* Copy cropped image line by line */ - for (int16_t curr_y = offset_y; - curr_y < offset_y + count_y; - curr_y++) { - uint16_t *curr_img = - img + (curr_y * width + offset_x); - gfx_copy_region( - &display_screen, - pos_x + offset_x, - pos_y + curr_y, - count_x, - 1, - GFX_RAW, - count_x * 2, - curr_img - ); - } } - - return 0; } + + return 0; } int epic_disp_line( diff --git a/epicardium/modules/panic.c b/epicardium/modules/panic.c index ed4575a5611a164145c762efdb86a46a6e39ee4f..b365b223db9cf75bc72c8866c77e9d82c9b4c502 100644 --- a/epicardium/modules/panic.c +++ b/epicardium/modules/panic.c @@ -120,13 +120,12 @@ static void faultsplash(const char *msg) { LCD_SetBacklight(100); - gfx_copy_region( + gfx_copy_region_rle_mono( &display_screen, 0, 0, 160, 80, - GFX_RLE_MONO, sizeof(faultsplash_rle), faultsplash_rle ); diff --git a/hw-tests/dual-core/main.c b/hw-tests/dual-core/main.c index 30026b8051f1e64f775dbcee8df46fd7b1d8615b..3de7f47152e0d9386c0bbb2212022c90cba91195 100644 --- a/hw-tests/dual-core/main.c +++ b/hw-tests/dual-core/main.c @@ -28,8 +28,7 @@ int main(void) 0, 160, 80, - GFX_RAW, - sizeof(Heart), + GFX_RGB565, (const void *)(Heart) ); gfx_update(&display_screen); diff --git a/hw-tests/hello-world/main.c b/hw-tests/hello-world/main.c index 5f2a82aad5b14a52cfa94ee3e3f6acf891a3dfa3..39a75e35f8027920692fa62a460d739bf720498d 100644 --- a/hw-tests/hello-world/main.c +++ b/hw-tests/hello-world/main.c @@ -36,8 +36,7 @@ int main(void) 0, 160, 80, - GFX_RAW, - sizeof(Heart), + GFX_RGB565, (const void *)(Heart) ); gfx_update(&display_screen); diff --git a/hw-tests/ips/main.c b/hw-tests/ips/main.c index b638a7832310f0d681c57f979001b595fe1e140c..e1ff5fa16a674efc17cc7880bf63115cf03fc8ef 100644 --- a/hw-tests/ips/main.c +++ b/hw-tests/ips/main.c @@ -43,8 +43,7 @@ int main(void) 0, 40, 40, - GFX_RAW, - 40 * 40 * 2, + GFX_RGB565, (const void *)(gImage_40X40) ); gfx_copy_region( @@ -53,8 +52,7 @@ int main(void) 0, 160, 80, - GFX_RAW, - 160 * 80 * 2, + GFX_RGB565, (const void *)(gImage_160X80) ); gfx_update(&display_screen); diff --git a/lib/gfx/framebuffer.c b/lib/gfx/framebuffer.c index 6c8dc3aac453214494b1758a6771a0ff3959ee8f..314f3e2f0a530a8083cfb0863df0428aa89dd192 100644 --- a/lib/gfx/framebuffer.c +++ b/lib/gfx/framebuffer.c @@ -99,3 +99,21 @@ void fb_setpixel(struct framebuffer *fb, int x, int y, Color c) pixel[0] = color[1]; } } + +Color fb_getpixel(struct framebuffer *fb, int x, int y) +{ + uint8_t *pixel = fb_pixel(fb, x, y); + if (pixel == NULL) + return 0; + + Color c; + uint8_t *color = (uint8_t *)(&c); + const size_t bpp = fb_bytes_per_pixel(fb); + switch (bpp) { + default: + case 2: + color[0] = pixel[1]; + color[1] = pixel[0]; + } + return c; +} diff --git a/lib/gfx/framebuffer.h b/lib/gfx/framebuffer.h index e2358c73464e7af26cc287c2348c537e5da15bcd..0bd0a593af0bb060d92318aa896cde8165f6775f 100644 --- a/lib/gfx/framebuffer.h +++ b/lib/gfx/framebuffer.h @@ -27,6 +27,7 @@ struct framebuffer { size_t fb_bytes_per_pixel(const struct framebuffer *fb); void *fb_pixel(struct framebuffer *fb, int x, int y); void fb_setpixel(struct framebuffer *fb, int x, int y, Color c); +Color fb_getpixel(struct framebuffer *fb, int x, int y); void fb_clear_to_color(struct framebuffer *fb, Color c); void fb_clear(struct framebuffer *fb); Color fb_encode_color_rgb(struct framebuffer *fb, int r, int g, int b); diff --git a/lib/gfx/gfx.c b/lib/gfx/gfx.c index 886ecdeeb99b6054ca718d05342438ade33eb25e..172dc9079851b93fb19aed37d3cbaf1107fedf26 100644 --- a/lib/gfx/gfx.c +++ b/lib/gfx/gfx.c @@ -13,6 +13,42 @@ const struct gfx_color_rgb gfx_colors_rgb[COLORS] = { { 255, 255, 0 } /* YELLOW */ }; +static inline uint16_t rgb8_to_rgb565(const uint8_t *bytes) +{ + return ((bytes[0] & 0b11111000) << 8) | ((bytes[1] & 0b11111100) << 3) | + (bytes[2] >> 3); +} + +static inline void rgb565_to_rgb8(uint16_t c, uint8_t *bytes) +{ + bytes[0] = (c & 0b1111100000000000) >> 8; + bytes[1] = (c & 0b0000011111100000) >> 3; + bytes[2] = (c & 0b0000000000011111) << 3; +} + +static inline uint16_t rgba5551_to_rgb565(const uint8_t *bytes) +{ + return (bytes[1] << 8) | (bytes[0] & 0b11000000) | + ((bytes[0] & 0b00111110) >> 1); +} + +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; +} + void gfx_setpixel(struct gfx_region *r, int x, int y, Color c) { if (x < 0 || y < 0) @@ -23,6 +59,25 @@ void gfx_setpixel(struct gfx_region *r, int x, int y, Color c) fb_setpixel(r->fb, r->x + x, r->y + y, c); } +void gfx_setpixel_rgba(struct gfx_region *r, int x, int y, const uint8_t *c) +{ + if (x < 0 || y < 0) + return; + if ((size_t)x >= r->width || (size_t)y >= r->height) + return; + + uint8_t pixel[3]; + Color p = fb_getpixel(r->fb, r->x + x, r->y + y); + rgb565_to_rgb8(p, pixel); + + uint8_t alpha = c[3]; + pixel[0] = apply_alpha(c[0], pixel[0], alpha); + pixel[1] = apply_alpha(c[1], pixel[1], alpha); + pixel[2] = apply_alpha(c[2], pixel[2], alpha); + + fb_setpixel(r->fb, r->x + x, r->y + y, rgb8_to_rgb565(pixel)); +} + struct gfx_region gfx_screen(struct framebuffer *fb) { struct gfx_region r = { .fb = fb, @@ -270,61 +325,48 @@ Color gfx_color(struct gfx_region *reg, enum gfx_color color) return gfx_color_rgb(reg, c->r, c->g, c->b); } -static void gfx_copy_region_raw( +void gfx_copy_region( struct gfx_region *reg, int x, int y, int w, int h, - size_t size, - const void *p + enum gfx_encoding encoding, + const uint8_t *p ) { - size_t bpp = size / (w * h); - for (int y_ = 0; y_ < h; y_++) { for (int x_ = 0; x_ < w; x_++) { Color c; + uint8_t alpha; - switch (bpp) { - default: - case 2: + switch (encoding) { + case GFX_RGB565: + /* Assuming alignment here */ c = *(const uint16_t *)(p); + gfx_setpixel(reg, x + x_, y + y_, c); + p += 2; + break; + case GFX_RGB8: + c = rgb8_to_rgb565(p); + gfx_setpixel(reg, x + x_, y + y_, c); + p += 3; + break; + case GFX_RGBA5551: + c = rgba5551_to_rgb565(p); + alpha = (*p) & 1; + if (alpha) { + gfx_setpixel(reg, x + x_, y + y_, c); + } + p += 2; + break; + case GFX_RGBA8: + gfx_setpixel_rgba(reg, x + x_, y + y_, p); + p += 4; + break; + default: + return; break; } - - gfx_setpixel(reg, x + x_, y + y_, c); - p += bpp; - } - } -} - -static void gfx_copy_region_mono( - struct gfx_region *reg, - int x, - int y, - int w, - int h, - size_t size, - const void *p -) { - const char *bp = p; - int bit = 0; - Color white = gfx_color(reg, WHITE); - Color black = gfx_color(reg, BLACK); - - for (int y_ = 0; y_ < h; y_++) { - for (int x_ = 0; x_ < w; x_++) { - int value = *bp & (1 << bit); - if (++bit >= 8) { - bp++; - bit %= 8; - - if ((const void *)(bp) >= (p + size)) - return; - } - - Color c = value ? white : black; - gfx_setpixel(reg, x + x_, y + y_, c); } } } @@ -336,7 +378,7 @@ static void gfx_copy_region_mono( * significant bit determines the color, the remaining 7 bits determine the * amount. */ -static void gfx_copy_region_rle_mono( +void gfx_copy_region_rle_mono( struct gfx_region *reg, int x, int y, @@ -363,31 +405,6 @@ static void gfx_copy_region_rle_mono( } } -void gfx_copy_region( - struct gfx_region *reg, - int x, - int y, - int w, - int h, - enum gfx_encoding encoding, - size_t size, - const void *p -) { - switch (encoding) { - case GFX_RAW: - gfx_copy_region_raw(reg, x, y, w, h, size, p); - break; - case GFX_MONO: - gfx_copy_region_mono(reg, x, y, w, h, size, p); - break; - case GFX_RLE_MONO: - gfx_copy_region_rle_mono(reg, x, y, w, h, size, p); - break; - default: - break; - } -} - void gfx_copy_raw(struct gfx_region *reg, const void *p, size_t size) { fb_copy_raw(reg->fb, p, size); diff --git a/lib/gfx/gfx.h b/lib/gfx/gfx.h index 4c9a16e508fef1ac4d7346d7adeeee62d71d5c1f..c785b73d8202ba8fd62b7a88ee09bf9c3d937c9e 100644 --- a/lib/gfx/gfx.h +++ b/lib/gfx/gfx.h @@ -44,9 +44,12 @@ enum gfx_color { }; enum gfx_encoding { - GFX_RAW, + GFX_RGB565, GFX_MONO, - GFX_RLE_MONO + GFX_RLE_MONO, + GFX_RGB8, + GFX_RGBA8, + GFX_RGBA5551 }; struct gfx_color_rgb { @@ -57,9 +60,10 @@ struct gfx_color_rgb { Color gfx_color(struct gfx_region *reg, enum gfx_color color); -void gfx_copy_region(struct gfx_region *reg, int x, int y, int w, int h, - enum gfx_encoding encoding, size_t size, - const void *p); +void gfx_copy_region_rle_mono(struct gfx_region *reg, int x, int y, int w, int h, + size_t size, const void *p); +void gfx_copy_region( struct gfx_region *reg, int x, int y, int w, int h, + enum gfx_encoding encoding, const uint8_t *p); void gfx_copy_raw(struct gfx_region *reg, const void *p, size_t size); #endif diff --git a/lib/lodepng/lodepng b/lib/lodepng/lodepng new file mode 160000 index 0000000000000000000000000000000000000000..7fdcc96a5e5864eee72911c3ca79b1d9f0d12292 --- /dev/null +++ b/lib/lodepng/lodepng @@ -0,0 +1 @@ +Subproject commit 7fdcc96a5e5864eee72911c3ca79b1d9f0d12292 diff --git a/lib/lodepng/lodepng.c b/lib/lodepng/lodepng.c new file mode 120000 index 0000000000000000000000000000000000000000..c5f0d591106af0a8f58d21b0c5194e8759ae05ed --- /dev/null +++ b/lib/lodepng/lodepng.c @@ -0,0 +1 @@ +lodepng/lodepng.cpp \ No newline at end of file diff --git a/lib/lodepng/meson.build b/lib/lodepng/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..5d50b024b932005afaecaa7eab4994700cfd481f --- /dev/null +++ b/lib/lodepng/meson.build @@ -0,0 +1,19 @@ +includes = include_directories( + './lodepng', +) + +sources = files( + './lodepng.c', +) + +lib = static_library( + 'lodepng', + sources, + include_directories: includes, + c_args: ['-O3', '-w', '-DLODEPNG_NO_COMPILE_ENCODER', '-DLODEPNG_NO_COMPILE_DISK', '-DLODEPNG_NO_COMPILE_ALLOCATORS'], +) + +lodepng = declare_dependency( + include_directories: includes, + link_with: lib, +) diff --git a/lib/meson.build b/lib/meson.build index 86d05cc0678947c81845e658b983d7a8c023d4d5..7f094776ff8feb56a9faa009ac06cf64df2b6804 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -17,3 +17,4 @@ subdir('./crypto/') subdir('./card10/') subdir('./mx25lba/') subdir('./ff13/') +subdir('./lodepng/') 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/display.py b/pycardium/modules/py/display.py index 1c94d47b35844db5272447265c94389c2eac918b..4884b1e7d1be63a8487f2088b219ca641035dcb7 100644 --- a/pycardium/modules/py/display.py +++ b/pycardium/modules/py/display.py @@ -8,6 +8,11 @@ FONT16 = 2 FONT20 = 3 FONT24 = 4 +RGB8 = 0 +RGBA8 = 1 +RGB565 = 2 +RGBA5551 = 3 + class Display: """ @@ -120,7 +125,7 @@ class Display: sys_display.pixel(x, y, col) return self - def blit(self, x, y, w, h, img, rgb565=False, alpha=None): + def blit(self, x, y, w, h, img, format=RGB8): """ Draws an image on the display. @@ -128,16 +133,20 @@ class Display: :param y: Y coordinate :param w: Image width :param h: Image height - :param img: Buffer with pixel data. Default format is RGB with 8 bits per channel. - :param alpha: Alpha mask for `img` - :param rgb565: Set to `True` if the data supplied is in rgb565 format instead of 8 bit RGB. + :param img: Buffer with pixel data + :param format: Format of the RGB data. One of ``display.RGB8``, `` + + - ``display.RGB8``: 24 bit RGB. + - ``display.RGBA8``: 24 bit RGB + 8 bit alpha. + - ``display.RGB565``: 16 bit RGB. This consumes 1 byte less RAM per pixel than ``display.RGB8``. + - ``display.RGBA5551``: 15 bit RGB + 1 bit alpha. + + Default is ``display.RGB8``. - .. note:: - Alpha mask support is not yet implemented. .. versionadded:: 1.17 - **Example with RGB data:** + **Example with RGB8 data:** .. code-block:: python @@ -153,7 +162,7 @@ class Display: d.update() - **Example with rgb565 data:** + **Example with RGB565 data:** .. code-block:: python @@ -165,7 +174,7 @@ class Display: img = array.array('H', [0x07E0 for x in range(32 * 20)]) with display.open() as d: d.clear() - d.blit(10, 10, 32, 20, img, rgb565=True) + d.blit(10, 10, 32, 20, img, format=display.RGB565) d.update() @@ -180,20 +189,13 @@ class Display: f = framebuf.FrameBuffer(bytearray(160 * 80 * 2), 160, 80, framebuf.RGB565) with display.open() as d: f.text("Hello World", 0, 0, 0xF800) # red - d.blit(0, 0, 160, 80, f, rgb565=True) + d.blit(0, 0, 160, 80, f, format=display.RGB565) d.update() """ - # TODO: alpha is not yet supported by epicardium - if alpha is not None: - raise ValueError("alpha not yet supported") - - if alpha is None: - sys_display.blit(x, y, w, h, img, rgb565) - else: - sys_display.blit(x, y, w, h, img, rgb565, alpha) + sys_display.blit(x, y, w, h, img, format) return self 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..a5c460690f867a11266b69886777cb3aac2d2343 --- /dev/null +++ b/pycardium/modules/py/png.py @@ -0,0 +1,81 @@ +import sys_png +import color + +RGB8 = 0 +RGBA8 = 1 +RGB565 = 2 +RGBA5551 = 3 + + +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: + + - ``png.RGB8``: 24 bit RGB. + - ``png.RGBA8``: 24 bit RGB + 8 bit alpha. + - ``png.RGB565``: 16 bit RGB. This consumes 1 byte less RAM per pixel than ``png.RGB8``. + - ``png.RGBA5551``: 15 bit RGB + 1 bit alpha. + + Default is ``png.RGB8``. + + :param Color bg: Background color. + + If the PNG contains an alpha channel but no alpha + channel is requested in the output (``png.RGB8`` or ``png.RGB565``) + this color will be used as the background color. + + Default is ``Color.BLACK``. + + :returns: Typle ``(width, height, data)`` + + .. versionadded:: 1.17 + + **Example with RGBA8 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(), png.RGBA8) + f.close() + with display.open() as d: + d.clear() + d.blit(0, 0, w, h, img, display.RGBA8) + 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(), png.RGB565) + f.close() + with display.open() as d: + d.clear() + d.blit(0, 0, w, h, img, True, display.RGB565) + d.update() + + """ + + formats = (RGB8, RGBA8, RGB565, RGBA5551) + if format not in formats: + raise ValueError("Output format not supported") + + if format == RGB8: + return sys_png.decode(png_data, 1, 0, bg) + if format == RGBA8: + return sys_png.decode(png_data, 1, 1, bg) + if format == RGB565: + return sys_png.decode(png_data, 0, 0, bg) + if format == RGBA5551: + return sys_png.decode(png_data, 0, 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_display.c b/pycardium/modules/sys_display.c index 37011cae8bc7ccf389815cf5dfacd5bccfbc0b4b..60a99aadd85aa895250e8ab5ea5a0af1b2f02961 100644 --- a/pycardium/modules/sys_display.c +++ b/pycardium/modules/sys_display.c @@ -97,14 +97,11 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN( /* blit image to display */ static mp_obj_t mp_display_blit(size_t n_args, const mp_obj_t *args) { - /* Required arguments: posx, posy (on display), - width, height (of image), - buffer (rgb data of image) */ - int pos_x = mp_obj_get_int(args[0]); - int pos_y = mp_obj_get_int(args[1]); - int width = mp_obj_get_int(args[2]); - int height = mp_obj_get_int(args[3]); - bool rgb565 = mp_obj_is_true(args[5]); + int pos_x = mp_obj_get_int(args[0]); + int pos_y = mp_obj_get_int(args[1]); + int width = mp_obj_get_int(args[2]); + int height = mp_obj_get_int(args[3]); + enum epic_rgb_format format = mp_obj_get_int(args[5]); mp_buffer_info_t img; int res = 0; @@ -114,46 +111,7 @@ static mp_obj_t mp_display_blit(size_t n_args, const mp_obj_t *args) mp_raise_TypeError("'img' does not support buffer protocol."); } - int bpp = rgb565 ? 2 : 3; - if ((int)img.len < width * height * bpp) { - mp_raise_ValueError("'img' is too small."); - } - - uint16_t *buf = NULL; - if (rgb565) { - buf = (uint16_t *)img.buf; - } else { - /* Will raise an exception if out of memory: */ - buf = m_malloc(width * height * bpp); - for (int i = 0; i < width * height; i++) { - buf[i] = rgb888_to_rgb565(((uint8_t *)img.buf) + 3 * i); - } - } - - if (n_args > 6) { - mp_buffer_info_t alpha; - - /* Load alpha buffer and check size */ - if (!mp_get_buffer(args[6], &alpha, MP_BUFFER_READ)) { - mp_raise_TypeError( - "'alpha' does not support buffer protocol." - ); - } - if ((int)alpha.len < width * height) { - mp_raise_ValueError("'alpha' is too small."); - } - - res = epic_disp_blit( - pos_x, pos_y, width, height, buf, (uint8_t *)alpha.buf - ); - } else { - res = epic_disp_blit(pos_x, pos_y, width, height, buf, NULL); - } - - if (!rgb565) { - /* Do not free rgb565 data. It is owned by the caller */ - m_free(buf); - } + res = epic_disp_blit(pos_x, pos_y, width, height, img.buf, format); if (res < 0) { mp_raise_OSError(-res); @@ -162,7 +120,7 @@ static mp_obj_t mp_display_blit(size_t n_args, const mp_obj_t *args) return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN( - display_blit_obj, 5, 6, mp_display_blit + display_blit_obj, 6, 6, mp_display_blit ); /* set display backlight brightness */ diff --git a/pycardium/modules/sys_png.c b/pycardium/modules/sys_png.c new file mode 100644 index 0000000000000000000000000000000000000000..c6dded0987e0ec6103b4a43caf30d94c9ce844e0 --- /dev/null +++ b/pycardium/modules/sys_png.c @@ -0,0 +1,193 @@ +#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 rgba8_to_rgba5551(uint8_t *bytes) +{ + uint16_t c = ((bytes[0] & 0b11111000) << 8) | + ((bytes[1] & 0b11111000) << 3) | + ((bytes[2] & 0b11111000) >> 2); + if (bytes[3] > 127) { + c |= 1; + } + return c; +} + +static inline uint16_t rgb8_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 rgb8_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(rgb8_out)) { + if (mp_obj_is_true(alpha_out)) { + for (i = 0, j = 0; i < raw_len; i += 4, j += 2) { + uint16_t c = rgba8_to_rgba5551(&raw[i]); + raw[j] = c & 0xFF; + raw[j + 1] = c >> 8; + raw[j + 2] = raw[i + 3]; + } + } else { + for (i = 0, j = 0; i < raw_len; i += 3, j += 2) { + uint16_t c = rgb8_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) /* diff --git a/tools/code-style.sh b/tools/code-style.sh index 3256654a089239c56da6c24bf427ad8ab2140943..311df9bea8f01ccab1497ccd3fdace27a54a89a7 100755 --- a/tools/code-style.sh +++ b/tools/code-style.sh @@ -41,6 +41,7 @@ formatter_blacklist=( lib/ff13/ lib/FreeRTOS/ lib/FreeRTOS-Plus/ + lib/lodepng/ lib/micropython/ lib/mx25lba/ lib/sdk/