From 1ca2d39325627185174b2e9e06bdacb5cbcbfd16 Mon Sep 17 00:00:00 2001 From: BO KAI HUANG Date: Thu, 5 Dec 2024 10:38:51 +0800 Subject: [PATCH] Support TinyVG format decoding and rendering Implemented support for decoding TinyVG format by parsing commands step-by-step. Each decoded command is executed using twin's path functions to render graphics. This enhances twin's capability to handle compact vector graphics . Ref: https://tinyvg.tech/download/specification.pdf Close #71 --- Makefile | 5 + apps/apps_image.h | 14 + apps/image.c | 122 ++++ apps/main.c | 4 + assets/chart.tvg | Bin 0 -> 6540 bytes assets/comic.tvg | Bin 0 -> 16864 bytes assets/flowchart.tvg | Bin 0 -> 6458 bytes assets/folder.tvg | Bin 0 -> 927 bytes assets/shield.tvg | Bin 0 -> 119 bytes assets/tiger.tvg | Bin 0 -> 27522 bytes configs/Kconfig | 10 + include/twin.h | 9 + src/image-tvg.c | 1298 ++++++++++++++++++++++++++++++++++++++++++ src/image.c | 15 + src/trig.c | 38 +- 15 files changed, 1500 insertions(+), 15 deletions(-) create mode 100644 apps/apps_image.h create mode 100644 apps/image.c create mode 100644 assets/chart.tvg create mode 100644 assets/comic.tvg create mode 100644 assets/flowchart.tvg create mode 100644 assets/folder.tvg create mode 100644 assets/shield.tvg create mode 100644 assets/tiger.tvg create mode 100644 src/image-tvg.c diff --git a/Makefile b/Makefile index 8a612ec..d7dd21f 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,10 @@ ifeq ($(CONFIG_LOADER_GIF), y) libtwin.a_files-y += src/image-gif.c endif +ifeq ($(CONFIG_LOADER_TVG), y) +libtwin.a_files-y += src/image-tvg.c +endif + # Applications libapps.a_files-y := apps/dummy.c @@ -96,6 +100,7 @@ libapps.a_files-$(CONFIG_DEMO_CALCULATOR) += apps/calc.c libapps.a_files-$(CONFIG_DEMO_LINE) += apps/line.c libapps.a_files-$(CONFIG_DEMO_SPLINE) += apps/spline.c libapps.a_files-$(CONFIG_DEMO_ANIMATION) += apps/animation.c +libapps.a_files-$(CONFIG_DEMO_IMAGE) += apps/image.c libapps.a_includes-y := include diff --git a/apps/apps_image.h b/apps/apps_image.h new file mode 100644 index 0000000..06320d5 --- /dev/null +++ b/apps/apps_image.h @@ -0,0 +1,14 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University + * All rights reserved. + */ + +#ifndef _APPS_IMAGE_H_ +#define _APPS_IMAGE_H_ + +#include + +void apps_image_start(twin_screen_t *screen, const char *name, int x, int y); + +#endif /* _APPS_ANIMATION_H_ */ diff --git a/apps/image.c b/apps/image.c new file mode 100644 index 0000000..064dc52 --- /dev/null +++ b/apps/image.c @@ -0,0 +1,122 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University + * All rights reserved. + */ + +#include + +#include "twin_private.h" + +#include "apps_image.h" + +#define _apps_image_pixmap(image) ((image)->widget.window->pixmap) +#define D(x) twin_double_to_fixed(x) +#define ASSET_PATH "assets/" +#define APP_WIDTH 400 +#define APP_HEIGHT 400 +typedef struct { + twin_widget_t widget; + twin_pixmap_t **pixes; + int image_idx; +} apps_image_t; + +static const char *tvg_files[] = { + /* https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/ */ + ASSET_PATH "tiger.tvg", + /* https://tinyvg.tech/img/chart.svg */ + ASSET_PATH "chart.tvg", + /* https://freesvg.org/betrayed */ + ASSET_PATH "comic.tvg", + /* https://github.com/PapirusDevelopmentTeam/papirus-icon-theme */ + ASSET_PATH "folder.tvg", + /* https://materialdesignicons.com/ */ + ASSET_PATH "shield.tvg", + /* https://tinyvg.tech/img/flowchart.png */ + ASSET_PATH "flowchart.tvg", +}; + +static void _apps_image_paint(apps_image_t *img) +{ + twin_operand_t srcop = { + .source_kind = TWIN_PIXMAP, + .u.pixmap = img->pixes[img->image_idx], + }; + + twin_composite(_apps_image_pixmap(img), 0, 0, &srcop, 0, 0, NULL, 0, 0, + TWIN_SOURCE, APP_WIDTH, APP_HEIGHT); +} + +static twin_dispatch_result_t _apps_image_dispatch(twin_widget_t *widget, + twin_event_t *event) +{ + apps_image_t *img = (apps_image_t *) widget; + if (_twin_widget_dispatch(widget, event) == TwinDispatchDone) + return TwinDispatchDone; + switch (event->kind) { + case TwinEventPaint: + _apps_image_paint(img); + break; + default: + break; + } + return TwinDispatchContinue; +} + +static void _apps_image_button_signal(maybe_unused twin_button_t *button, + twin_button_signal_t signal, + void *closure) +{ + if (signal != TwinButtonSignalDown) + return; + + apps_image_t *img = closure; + const int n = sizeof(tvg_files) / sizeof(tvg_files[0]); + img->image_idx = img->image_idx == n - 1 ? 0 : img->image_idx + 1; + if (!img->pixes[img->image_idx]) { + twin_pixmap_t *pix = twin_tvg_to_pixmap_scale( + tvg_files[img->image_idx], TWIN_ARGB32, APP_WIDTH, APP_HEIGHT); + if (!pix) + return; + img->pixes[img->image_idx] = pix; + } + _twin_widget_queue_paint(&img->widget); +} + +static void _apps_image_init(apps_image_t *img, + twin_box_t *parent, + twin_dispatch_proc_t dispatch) +{ + static twin_widget_layout_t preferred = {0, 0, 1, 1}; + preferred.height = parent->widget.window->screen->height * 3.0 / 4.0; + _twin_widget_init(&img->widget, parent, 0, preferred, dispatch); + img->image_idx = 0; + img->pixes = calloc(sizeof(tvg_files), sizeof(twin_pixmap_t *)); + img->pixes[0] = twin_tvg_to_pixmap_scale(tvg_files[0], TWIN_ARGB32, + APP_WIDTH, APP_HEIGHT); + twin_button_t *button = + twin_button_create(parent, "Next Image", 0xFF482722, D(10), + TwinStyleBold | TwinStyleOblique); + twin_widget_set(&button->label.widget, 0xFFFEE4CE); + button->signal = _apps_image_button_signal; + button->closure = img; + button->label.widget.shape = TwinShapeRectangle; +} + +static apps_image_t *apps_image_create(twin_box_t *parent) +{ + apps_image_t *img = malloc(sizeof(apps_image_t)); + + _apps_image_init(img, parent, _apps_image_dispatch); + return img; +} + +void apps_image_start(twin_screen_t *screen, const char *name, int x, int y) +{ + twin_toplevel_t *toplevel = + twin_toplevel_create(screen, TWIN_ARGB32, TwinWindowApplication, x, y, + APP_WIDTH, APP_HEIGHT, name); + apps_image_t *img = apps_image_create(&toplevel->box); + (void) img; + twin_toplevel_show(toplevel); +} diff --git a/apps/main.c b/apps/main.c index afd3dac..db3a33c 100644 --- a/apps/main.c +++ b/apps/main.c @@ -17,6 +17,7 @@ #include "apps_calc.h" #include "apps_clock.h" #include "apps_hello.h" +#include "apps_image.h" #include "apps_line.h" #include "apps_multi.h" #include "apps_spline.h" @@ -127,6 +128,9 @@ int main(void) apps_animation_start(tx->screen, "Viewer", ASSET_PATH "nyancat.gif", 20, 20); #endif +#if defined(CONFIG_DEMO_IMAGE) + apps_image_start(tx->screen, "Viewer", 20, 20); +#endif twin_dispatch(tx); diff --git a/assets/chart.tvg b/assets/chart.tvg new file mode 100644 index 0000000000000000000000000000000000000000..745021d0ae4c6e58fd579f79e1b0f47682b07a98 GIT binary patch literal 6540 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 literal 0 HcmV?d00001 diff --git a/assets/comic.tvg b/assets/comic.tvg new file mode 100644 index 0000000000000000000000000000000000000000..95676946009fa2f77fbdfd4f1f800f66d4a0dd81 GIT binary patch literal 16864 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 literal 0 HcmV?d00001 diff --git a/assets/flowchart.tvg b/assets/flowchart.tvg new file mode 100644 index 0000000000000000000000000000000000000000..9602b60c524fa471f27cbd743f5f00f069cdb5bf GIT binary patch literal 6458 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 literal 0 HcmV?d00001 diff --git a/assets/folder.tvg b/assets/folder.tvg new file mode 100644 index 0000000000000000000000000000000000000000..b70d0f37db82c1acd8168ae6f1dbe010b8eec7c1 GIT binary patch literal 927 zcmah{Jx|+E6uo{AKZoGPs7h%YiNpj5AtE7a@JE~`0fj`U5(q^oWuOB~e*gnPid3<5 zL+TIcQ1J&?x>Q0eRhP_NI&`Uom^{=&y;LYYFBnywYU|j?*XN#h-nEZ<%4C_!B#5Z; zs``F(eSJN8^YZfu$J3Sc=7XRH)3U0G^i?q{~Int5a@b-p25lVBDb0y#G)*amb~zL&^hu-{*&KI| zcJH9bB@^KK7&eowICi0lFpVPz? z?*DF<(L;!a1_qG8%Y-5~kq0l(48Gt@G|U+c@G%xk{co6ptEw@@alBhnN<4|n+uUL) zSUf86AMh%iIqLXX>Tp^N+aH$BTieTnb$(j7 zYHSyO)u}w$;1|z+)(6&ATETO?W-o-TI-pOpm|v{npj6| z%vMs*&sO6e65P07i=p$am~rr6*w*Uz3d4`e$~?2e%KkD6`J7K=s&H?rec-RucOdnhzIytcCz5-XRoHCFl=u`Kp}& literal 0 HcmV?d00001 diff --git a/assets/shield.tvg b/assets/shield.tvg new file mode 100644 index 0000000000000000000000000000000000000000..57033be662e75fbff11068f16f13a514ada7aae1 GIT binary patch literal 119 zcmV~$F%H5Y7zR+}t6N6~+AbuLY7N65264cK5UNc&H6eEK6yD2wxq1S8@BQ3GDf#dz zrTo4HoK<^J#cII~>ey}c!nEa>4Hr{FSgtE&oL04T32khG93kDolEux=ZXWRn_c$dI WxCq~P1>ZYE24|y>T0ysiAi@5pu^2f3 literal 0 HcmV?d00001 diff --git a/assets/tiger.tvg b/assets/tiger.tvg new file mode 100644 index 0000000000000000000000000000000000000000..a3d3bd960a9f9500fe9952e7b3fea9e1aa98416f GIT binary patch literal 27522 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 literal 0 HcmV?d00001 diff --git a/configs/Kconfig b/configs/Kconfig index d85b63d..1a601fc 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -71,6 +71,10 @@ config LOADER_GIF bool "Enable GIF loader" default y +config LOADER_TVG + bool "Enable TinyVG (TVG) loader" + default y + endmenu menu "Demo Applications" @@ -113,4 +117,10 @@ config DEMO_ANIMATION bool "Build animation demo" default y depends on DEMO_APPLICATIONS + +config DEMO_IMAGE + bool "Build scalable image demo" + select LOADER_TVG + default y + depends on DEMO_APPLICATIONS endmenu diff --git a/include/twin.h b/include/twin.h index 38a26cd..382aa2d 100644 --- a/include/twin.h +++ b/include/twin.h @@ -1186,6 +1186,15 @@ twin_work_t *twin_set_work(twin_work_proc_t work_proc, void twin_clear_work(twin_work_t *work); +/* + * image-tvg.c + */ + +twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath, + twin_format_t fmt, + twin_coord_t w, + twin_coord_t h); + /* * backend */ diff --git a/src/image-tvg.c b/src/image-tvg.c new file mode 100644 index 0000000..e518ee5 --- /dev/null +++ b/src/image-tvg.c @@ -0,0 +1,1298 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2024 National Cheng Kung University, Taiwan + * All rights reserved. + */ +#include +#include +#include +#include +#include +#include + +#include "twin.h" +#include "twin_private.h" + +#define D(x) twin_double_to_fixed(x) +#define GET_COLOR(ctx, idx) ctx->colors[idx] +#define PIXEL_ARGB(a, r, g, b) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) + +enum { + /* end of document This command determines the end of file. */ + TVG_CMD_END_DOCUMENT = 0, + /* fill polygon This command fills an N-gon.*/ + TVG_CMD_FILL_POLYGON, + /* fill rectangles This command fills a set of rectangles.*/ + TVG_CMD_FILL_RECTANGLES, + /* fill path This command fills a free-form path.*/ + TVG_CMD_FILL_PATH, + /* draw lines This command draws a set of lines.*/ + TVG_CMD_DRAW_LINES, + /* draw line loop This command draws the outline of a polygon.*/ + TVG_CMD_DRAW_LINE_LOOP, + /* draw line strip This command draws a list of end-to-end lines.*/ + TVG_CMD_DRAW_LINE_STRIP, + /* draw line path This command draws a free-form path. */ + TVG_CMD_DRAW_LINE_PATH, + /* outline fill polygon This command draws a filled polygon with an outline. + */ + TVG_CMD_OUTLINE_FILL_POLYGON, + /* + * outline fill rectangles This command draws several filled + * rectangles with an outline. + */ + TVG_CMD_OUTLINE_FILL_RECTANGLES, + /* + * outline fill path This command combines the fill and draw + * line path command into one + */ + TVG_CMD_OUTLINE_FILL_PATH +}; + +enum { + /* solid color */ + TVG_STYLE_FLAT = 0, + /* linear gradient */ + TVG_STYLE_LINEAR, + /* radial gradient */ + TVG_STYLE_RADIAL +}; + +enum { + /* unit uses 16 bit */ + TVG_RANGE_DEFAULT = 0, + /* unit takes only 8 bit */ + TVG_RANGE_REDUCED, + /* unit uses 32 bit */ + TVG_RANGE_ENHANCED, +}; + +/* + * The color encoding used in a TinyVG file. + * This enum describes how the data in the color table + * section of the format looks like. + */ +enum { + TVG_COLOR_U8888 = 0, + TVG_COLOR_U565, + TVG_COLOR_F32, + TVG_COLOR_CUSTOM, +}; + +/* + * A TinyVG scale value defines the scale for all units inside a graphic. + * The scale is defined by the number of decimal bits in a `i32`, thus scaling + * can be trivially implemented by shifting the integers right by the scale + * bits. + */ +enum { + TVG_SCALE_1_1 = 0, + TVG_SCALE_1_2, + TVG_SCALE_1_4, + TVG_SCALE_1_8, + TVG_SCALE_1_16, + TVG_SCALE_1_32, + TVG_SCALE_1_64, + TVG_SCALE_1_128, + TVG_SCALE_1_256, + TVG_SCALE_1_512, + TVG_SCALE_1_1024, + TVG_SCALE_1_2048, + TVG_SCALE_1_4096, + TVG_SCALE_1_8192, + TVG_SCALE_1_16384, + TVG_SCALE_1_32768, +}; + +/* path commands */ +enum { + TVG_PATH_LINE = 0, + TVG_PATH_HLINE, + TVG_PATH_VLINE, + TVG_PATH_CUBIC, + TVG_PATH_ARC_CIRCLE, + TVG_PATH_ARC_ELLIPSE, + TVG_PATH_CLOSE, + TVG_PATH_QUAD +}; + +enum { + TVG_SUCCESS = 0, + TVG_E_INVALID_ARG, + TVG_E_INVALID_STATE, + TVG_E_INVALID_FORMAT, + TVG_E_IO_ERROR, + TVG_E_OUT_OF_MEMORY, + TVG_E_NOT_SUPPORTED +}; + +/* clamp a value to a range */ +#define TVG_CLAMP(x, mn, mx) (x > mx ? mx : (x < mn ? mn : x)) +/* get the red channel of an RGB565 color */ +#define TVG_RGB16_R(x) (x & 0x1F) +/* get the green channel of an RGB565 color */ +#define TVG_RGB16_G(x) ((x >> 5) & 0x3F) +/* get the blue channel of an RGB565 color */ +#define TVG_RGB16_B(x) ((x >> 11) & 0x1F) +/* + * get the index of the command + * essentially the command id + */ +#define TVG_CMD_INDEX(x) (x & 0x3F) +/* get the style kind flags in the command */ +#define TVG_CMD_STYLE_KIND(x) ((x >> 6) & 0x3) +/* + * get the packed size out of the size + * and style kind packed value + */ +#define TVG_SIZE_AND_STYLE_SIZE(x) ((x & 0x3F) + 1) +/* + * get the style kind out of the size + * and style kind packed value + */ +#define TVG_SIZE_AND_STYLE_STYLE_KIND(x) ((x >> 6) & 0x3) +/* get the scale from the header */ +#define TVG_HEADER_DATA_SCALE(x) (x & 0x0F) +/* get the color encoding from the header */ +#define TVG_HEADER_DATA_COLOR_ENC(x) ((x >> 4) & 0x03) +/* get the color range from the header */ +#define TVG_HEADER_DATA_RANGE(x) ((x >> 6) & 0x03) +/* get the path command index/id */ +#define TVG_PATH_CMD_INDEX(x) (x & 0x7) +/* flag indicating the path has line/stroke to it */ +#define TVG_PATH_CMD_HAS_LINE(x) ((x >> 4) & 0x1) +/* flag indicating the arc is a large arc */ +#define TVG_ARC_LARGE(x) (x & 0x1) +/* flag indicating the sweep direction 0=left, 1=right */ +#define TVG_ARC_SWEEP(x) ((x >> 1) & 1) + +/* F32 pixel format */ +typedef struct { + float r, g, b, a; +} tvg_f32_pixel_t; + +/* rgba32 color struct */ +typedef struct { + uint8_t r, g, b, a; +} tvg_rgba32_t; + +/* TVG internal color struct */ +typedef struct { + float r, g, b; +} tvg_rgb_t; + +/* TVG internal color struct with alpha channel */ +typedef struct { + float r, g, b, a; +} tvg_rgba_t; + +/* coordinate */ +typedef struct { + float x, y; +} tvg_point_t; + +/* rectangle */ +typedef struct { + float x, y; + float width, height; +} tvg_rect_t; + +/* gradient data */ +typedef struct { + tvg_point_t point0; + tvg_point_t point1; + uint32_t color0; + uint32_t color1; +} tvg_gradient_t; + +/* style data */ +typedef struct { + uint8_t kind; + union { + uint32_t flat; + tvg_gradient_t linear; + tvg_gradient_t radial; + }; +} tvg_style_t; + +/* fill header */ +typedef struct { + tvg_style_t style; + size_t size; +} tvg_fill_header_t; + +/* line header */ +typedef struct { + tvg_style_t style; + float line_width; + size_t size; +} tvg_line_header_t; + +/* line and fill header */ +typedef struct { + tvg_style_t fill_style; + tvg_style_t line_style; + float line_width; + size_t size; +} tvg_line_fill_header_t; + +/* used to provide an input cursor over an arbitrary source */ +typedef size_t (*tvg_input_func_t)(uint8_t *data, size_t size, void *state); + +typedef struct { + /* the input source */ + tvg_input_func_t in; + /* the user defined input state */ + void *in_state; + /* the target pixmap */ + twin_pixmap_t *pixmap; + twin_path_t *path; + /* the scaling used */ + uint8_t scale; + /* the color encoding */ + uint8_t color_encoding; + /* the coordinate range */ + uint8_t coord_range; + /* the width and height of the drawing */ + uint32_t width, height; + /* the size of the color table */ + size_t colors_size; + /* the color table (must be freed) */ + twin_argb32_t *colors; + uint32_t current_color; +} tvg_context_t; + +/* + * the result type. TVG_SUCCESS = OK + * anything else, is a TVG_E_xxxx error + * code + */ +typedef int tvg_result_t; + +#define __goto_if_fail(res, label, msg) \ + do { \ + if (res != TVG_SUCCESS) { \ + log_error("%s, error = [%d]", msg, res); \ + goto label; \ + } \ + } while (0); + +#define __return_val_if_fail(res, msg) \ + do { \ + if (res != TVG_SUCCESS) { \ + log_error("%s, error = [%d]", msg, res); \ + return res; \ + } \ + } while (0); + +#define READ_VALUE(read, buffer, size) \ + do { \ + read = ctx->in((uint8_t *) buffer, size, ctx->in_state); \ + if (size > read) { \ + return TVG_E_IO_ERROR; \ + } \ + } while (0); + +static uint32_t tvg_map_zero_to_max(tvg_context_t *ctx, uint32_t value) +{ + if (value) + return value; + switch (ctx->coord_range) { + case TVG_RANGE_DEFAULT: + return 0xFFFF; + case TVG_RANGE_REDUCED: + return 0xFF; + default: + return 0xFFFFFFFF; + } +} + +static tvg_result_t tvg_read_coord(tvg_context_t *ctx, uint32_t *out_raw_value) +{ + size_t read; + switch (ctx->coord_range) { + case TVG_RANGE_DEFAULT: { + uint16_t u16; + READ_VALUE(read, &u16, 2); + *out_raw_value = u16; + return TVG_SUCCESS; + } + case TVG_RANGE_REDUCED: { + uint8_t u8; + READ_VALUE(read, &u8, 1); + *out_raw_value = u8; + return TVG_SUCCESS; + } + default: + READ_VALUE(read, out_raw_value, 4); + return TVG_SUCCESS; + } +} + +static tvg_result_t tvg_read_color(tvg_context_t *ctx, twin_argb32_t *out_color) +{ + size_t read; + twin_fixed_t max_color_f = twin_int_to_fixed(255); + switch (ctx->color_encoding) { + case TVG_COLOR_F32: { + tvg_f32_pixel_t data; + READ_VALUE(read, &data, sizeof(tvg_f32_pixel_t)); + /* 255.0 * A */ + twin_fixed_t a_f = + twin_fixed_mul(max_color_f, twin_double_to_fixed(data.a)); + /* 255.0 * R */ + twin_fixed_t r_f = + twin_fixed_mul(max_color_f, twin_double_to_fixed(data.r)); + /* 255.0 * G */ + twin_fixed_t g_f = + twin_fixed_mul(max_color_f, twin_double_to_fixed(data.g)); + /* 255.0 * B */ + twin_fixed_t b_f = + twin_fixed_mul(max_color_f, twin_double_to_fixed(data.b)); + uint8_t a = (uint8_t) twin_fixed_to_int(a_f); + uint8_t r = (uint8_t) twin_fixed_to_int(r_f); + uint8_t g = (uint8_t) twin_fixed_to_int(g_f); + uint8_t b = (uint8_t) twin_fixed_to_int(b_f); + *out_color = PIXEL_ARGB(a, r, g, b); + return TVG_SUCCESS; + } + case TVG_COLOR_U565: { + uint16_t data; + READ_VALUE(read, &data, 2); + /* (255.0 * R) / 15.0 */ + twin_fixed_t r_f = twin_fixed_div( + twin_fixed_mul(max_color_f, + twin_double_to_fixed((float) TVG_RGB16_R(data))), + twin_int_to_fixed(15)); + /* (255.0 * G) / 31.0 */ + twin_fixed_t g_f = twin_fixed_div( + twin_fixed_mul(max_color_f, + twin_double_to_fixed((float) TVG_RGB16_G(data))), + twin_int_to_fixed(31)); + /* (255.0 * B) / 15.0 */ + twin_fixed_t b_f = twin_fixed_div( + twin_fixed_mul(max_color_f, + twin_double_to_fixed((float) TVG_RGB16_B(data))), + twin_int_to_fixed(15)); + uint8_t a = 0xff; + uint8_t r = (uint8_t) twin_fixed_to_int(r_f); + uint8_t g = (uint8_t) twin_fixed_to_int(g_f); + uint8_t b = (uint8_t) twin_fixed_to_int(b_f); + *out_color = PIXEL_ARGB(a, r, g, b); + return TVG_SUCCESS; + } + case TVG_COLOR_U8888: { + tvg_rgba32_t data; + READ_VALUE(read, &data.r, 1); + READ_VALUE(read, &data.g, 1); + READ_VALUE(read, &data.b, 1); + READ_VALUE(read, &data.a, 1); + *out_color = PIXEL_ARGB(data.a, data.r, data.g, data.b); + return TVG_SUCCESS; + } + case TVG_COLOR_CUSTOM: + return TVG_E_NOT_SUPPORTED; + default: + return TVG_E_INVALID_FORMAT; + } +} + +static float tvg_downscale_coord(tvg_context_t *ctx, uint32_t coord) +{ + uint16_t factor = (((uint16_t) 1) << ctx->scale); + return (float) coord / (float) factor; +} + +static tvg_result_t tvg_read_varuint(tvg_context_t *ctx, uint32_t *out_value) +{ + int count = 0; + uint32_t result = 0; + uint8_t byte; + size_t read = 0; + while (true) { + READ_VALUE(read, &byte, 1); + const uint32_t val = ((uint32_t) (byte & 0x7F)) << (7 * count); + result |= val; + if ((byte & 0x80) == 0) + break; + ++count; + } + *out_value = result; + return TVG_SUCCESS; +} + +static tvg_result_t tvg_read_unit(tvg_context_t *ctx, float *out_value) +{ + uint32_t val; + tvg_result_t res = tvg_read_coord(ctx, &val); + __return_val_if_fail(res, "Failed to read coordinate"); + *out_value = tvg_downscale_coord(ctx, val); + return TVG_SUCCESS; +} + +static tvg_result_t tvg_read_point(tvg_context_t *ctx, tvg_point_t *out_point) +{ + float f32; + tvg_result_t res = tvg_read_unit(ctx, &f32); + __return_val_if_fail(res, "Failed to read unit"); + out_point->x = f32; + res = tvg_read_unit(ctx, &f32); + __return_val_if_fail(res, "Failed to read unit"); + out_point->y = f32; + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_header(tvg_context_t *ctx, int dim_only) +{ + size_t read = 0; + uint8_t data[2]; + /* read the magic number */ + READ_VALUE(read, data, 2); + if (data[0] != 0x72 || data[1] != 0x56) { + return TVG_E_INVALID_FORMAT; + } + /* read the version */ + READ_VALUE(read, data, 1); + /* we only support version 1 */ + if (data[0] != 1) { + return TVG_E_NOT_SUPPORTED; + } + /* read the scale, encoding, and coordinate range: */ + READ_VALUE(read, data, 1); + /* it's all packed in a byte, so crack it up */ + ctx->scale = TVG_HEADER_DATA_SCALE(data[0]); + ctx->color_encoding = TVG_HEADER_DATA_COLOR_ENC(data[0]); + ctx->coord_range = TVG_HEADER_DATA_RANGE(data[0]); + /* now read the width and height: */ + uint32_t tmp; + tvg_result_t res = tvg_read_coord(ctx, &tmp); + __return_val_if_fail(res, "Failed to read coordinate"); + ctx->width = tvg_map_zero_to_max(ctx, tmp); + res = tvg_read_coord(ctx, &tmp); + __return_val_if_fail(res, "Failed to read coordinate"); + ctx->height = tvg_map_zero_to_max(ctx, tmp); + if (dim_only) { + return TVG_SUCCESS; + } + /* next read the color table */ + uint32_t color_count; + res = tvg_read_varuint(ctx, &color_count); + __return_val_if_fail(res, "Failed to read varuint"); + if (color_count == 0) { + return TVG_E_INVALID_FORMAT; + } + ctx->colors = malloc(color_count * sizeof(twin_argb32_t)); + if (!ctx->colors) { + return TVG_E_OUT_OF_MEMORY; + } + ctx->colors_size = (size_t) color_count; + for (size_t i = 0; i < ctx->colors_size; ++i) { + res = tvg_read_color(ctx, &ctx->colors[i]); + if (res != TVG_SUCCESS) { + free(ctx->colors); + ctx->colors = NULL; + return res; + } + } + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_gradient(tvg_context_t *ctx, + tvg_gradient_t *out_gradient) +{ + uint32_t u32; + tvg_point_t pt; + tvg_result_t res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + out_gradient->point0 = pt; + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + out_gradient->point1 = pt; + res = tvg_read_varuint(ctx, &u32); + __return_val_if_fail(res, "Failed to read varuint"); + out_gradient->color0 = u32; + if (u32 > ctx->colors_size) { + log_error("Invalid color index"); + return TVG_E_INVALID_FORMAT; + } + res = tvg_read_varuint(ctx, &u32); + __return_val_if_fail(res, "Failed to read varuint"); + if (u32 > ctx->colors_size) { + log_error("Invalid color index"); + return TVG_E_INVALID_FORMAT; + } + out_gradient->color1 = u32; + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_style(tvg_context_t *ctx, + int kind, + tvg_style_t *out_style) +{ + tvg_result_t res; + uint32_t flat; + tvg_gradient_t grad; + out_style->kind = kind; + switch (kind) { + case TVG_STYLE_FLAT: + res = tvg_read_varuint(ctx, &flat); + __return_val_if_fail(res, "Failed to read varuint"); + out_style->flat = flat; + break; + case TVG_STYLE_LINEAR: + res = tvg_parse_gradient(ctx, &grad); + __return_val_if_fail(res, "Failed to parse gradient"); + out_style->linear = grad; + break; + case TVG_STYLE_RADIAL: + res = tvg_parse_gradient(ctx, &grad); + __return_val_if_fail(res, "Failed to parse gradient"); + out_style->radial = grad; + break; + default: + res = TVG_E_INVALID_FORMAT; + break; + } + return res; +} + +static tvg_result_t tvg_parse_fill_header(tvg_context_t *ctx, + int kind, + tvg_fill_header_t *out_header) +{ + uint32_t u32; + tvg_result_t res = tvg_read_varuint(ctx, &u32); + __return_val_if_fail(res, "Failed to read varuint"); + size_t count = (size_t) u32 + 1; + out_header->size = count; + res = tvg_parse_style(ctx, kind, &out_header->style); + __return_val_if_fail(res, "Failed to parse style"); + return res; +} + +static tvg_result_t tvg_parse_line_header(tvg_context_t *ctx, + int kind, + tvg_line_header_t *out_header) +{ + uint32_t u32; + tvg_result_t res = tvg_read_varuint(ctx, &u32); + __return_val_if_fail(res, "Failed to read varuint"); + size_t count = (size_t) u32 + 1; + out_header->size = count; + res = tvg_parse_style(ctx, kind, &out_header->style); + __return_val_if_fail(res, "Failed to parse style"); + res = tvg_read_unit(ctx, &out_header->line_width); + __return_val_if_fail(res, "Failed to read unit"); + + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_line_fill_header( + tvg_context_t *ctx, + int kind, + tvg_line_fill_header_t *out_header) +{ + uint8_t d; + size_t read = 0; + READ_VALUE(read, &d, 1); + tvg_result_t res = TVG_SUCCESS; + + size_t count = TVG_SIZE_AND_STYLE_SIZE(d); + out_header->size = count; + res = tvg_parse_style(ctx, kind, &out_header->fill_style); + __return_val_if_fail(res, "Failed to parse style"); + res = tvg_parse_style(ctx, TVG_SIZE_AND_STYLE_STYLE_KIND(d), + &out_header->line_style); + __return_val_if_fail(res, "Failed to parse style"); + res = tvg_read_unit(ctx, &out_header->line_width); + __return_val_if_fail(res, "Failed to read unit"); + + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) +{ + tvg_result_t res = TVG_SUCCESS; + tvg_point_t start_point, cur_point, pt; + float f32; + uint8_t path_info; + twin_path_t *path = ctx->path; + res = tvg_read_point(ctx, &pt); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_move(path, D(pt.x), D(pt.y)); + start_point = pt; + cur_point = pt; + size_t read = 0; + for (size_t j = 0; j < size; ++j) { + READ_VALUE(read, &path_info, 1); + float line_width = 0.0f; + if (TVG_PATH_CMD_HAS_LINE(path_info)) { + res = tvg_read_unit(ctx, &line_width); + __goto_if_fail(res, error, "Failed to read unit"); + } + switch (TVG_PATH_CMD_INDEX(path_info)) { + case TVG_PATH_LINE: + res = tvg_read_point(ctx, &pt); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_draw(path, D(pt.x), D(pt.y)); + cur_point = pt; + break; + case TVG_PATH_HLINE: + res = tvg_read_unit(ctx, &f32); + __goto_if_fail(res, error, "Failed to read unit"); + pt.x = f32; + pt.y = cur_point.y; + twin_path_draw(path, D(pt.x), D(pt.y)); + cur_point = pt; + break; + case TVG_PATH_VLINE: + res = tvg_read_unit(ctx, &f32); + __goto_if_fail(res, error, "Failed to read unit"); + pt.x = cur_point.x; + pt.y = f32; + twin_path_draw(path, D(pt.x), D(pt.y)); + cur_point = pt; + break; + case TVG_PATH_CUBIC: { + tvg_point_t ctrl_point1, ctrl_point2, end_point; + res = tvg_read_point(ctx, &ctrl_point1); + __goto_if_fail(res, error, "Failed to read point"); + res = tvg_read_point(ctx, &ctrl_point2); + __goto_if_fail(res, error, "Failed to read point"); + res = tvg_read_point(ctx, &end_point); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_curve(path, D(ctrl_point1.x), D(ctrl_point1.y), + D(ctrl_point2.x), D(ctrl_point2.y), D(end_point.x), + D(end_point.y)); + cur_point = end_point; + } break; + case TVG_PATH_ARC_CIRCLE: { + uint8_t circle_info; + READ_VALUE(read, &circle_info, 1); + float radius; + res = tvg_read_unit(ctx, &radius); + __goto_if_fail(res, error, "Failed to read unit"); + res = tvg_read_point(ctx, &pt); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_arc_circle( + path, TVG_ARC_LARGE(circle_info), TVG_ARC_SWEEP(circle_info), + D(radius), D(cur_point.x), D(cur_point.y), D(pt.x), D(pt.y)); + cur_point = pt; + } break; + case TVG_PATH_ARC_ELLIPSE: { + uint8_t ellipse_info; + READ_VALUE(read, &ellipse_info, 1); + float radius_x, radius_y, rotation; + res = tvg_read_unit(ctx, &radius_x); + __goto_if_fail(res, error, "Failed to read unit"); + res = tvg_read_unit(ctx, &radius_y); + __goto_if_fail(res, error, "Failed to read unit"); + res = tvg_read_unit(ctx, &rotation); + __goto_if_fail(res, error, "Failed to read unit"); + res = tvg_read_point(ctx, &pt); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_arc_ellipse( + path, TVG_ARC_LARGE(ellipse_info), TVG_ARC_SWEEP(ellipse_info), + D(radius_x), D(radius_y), D(cur_point.x), D(cur_point.y), + D(pt.x), D(pt.y), rotation * TWIN_ANGLE_360 / 360); + cur_point = pt; + } break; + case TVG_PATH_CLOSE: + twin_path_draw(path, D(start_point.x), D(start_point.y)); + cur_point = start_point; + break; + case TVG_PATH_QUAD: { + tvg_point_t ctrl_point, end_point; + res = tvg_read_point(ctx, &ctrl_point); + __goto_if_fail(res, error, "Failed to read point"); + res = tvg_read_point(ctx, &end_point); + __goto_if_fail(res, error, "Failed to read point"); + twin_path_quadratic_curve(path, D(ctrl_point.x), D(ctrl_point.y), + D(end_point.x), D(end_point.y)); + cur_point = end_point; + } break; + default: + res = TVG_E_INVALID_FORMAT; + goto error; + } + } +error: + return res; +} + +static tvg_result_t tvg_parse_rect(tvg_context_t *ctx, tvg_rect_t *out_rect) +{ + tvg_point_t pt; + tvg_result_t res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + float w, h; + res = tvg_read_unit(ctx, &w); + __return_val_if_fail(res, "Failed to read unit"); + res = tvg_read_unit(ctx, &h); + __return_val_if_fail(res, "Failed to read unit"); + out_rect->x = pt.x; + out_rect->y = pt.y; + out_rect->width = w; + out_rect->height = h; + return TVG_SUCCESS; +} + +static void _stroke_path_with_style(tvg_context_t *ctx, + const tvg_style_t *fill_style, + twin_fixed_t pen_width) +{ + switch (fill_style->kind) { + case TVG_STYLE_FLAT: + twin_paint_stroke(ctx->pixmap, GET_COLOR(ctx, fill_style->flat), + ctx->path, pen_width); + break; + case TVG_STYLE_LINEAR: + /* TODO: Implement linear gradient color */ + twin_paint_stroke(ctx->pixmap, + GET_COLOR(ctx, fill_style->linear.color0), ctx->path, + pen_width); + break; + case TVG_STYLE_RADIAL: + /* TODO: Implement radial gradient color */ + twin_paint_stroke(ctx->pixmap, + GET_COLOR(ctx, fill_style->radial.color0), ctx->path, + pen_width); + break; + } +} + +static void _fill_path_with_style(tvg_context_t *ctx, + const tvg_style_t *fill_style) +{ + switch (fill_style->kind) { + case TVG_STYLE_FLAT: + twin_paint_path(ctx->pixmap, GET_COLOR(ctx, fill_style->flat), + ctx->path); + break; + case TVG_STYLE_LINEAR: + /* TODO: Implement linear gradient color */ + twin_paint_path(ctx->pixmap, GET_COLOR(ctx, fill_style->linear.color0), + ctx->path); + break; + case TVG_STYLE_RADIAL: + /* TODO: Implement radial gradient color */ + twin_paint_path(ctx->pixmap, GET_COLOR(ctx, fill_style->radial.color0), + ctx->path); + break; + } +} + +static tvg_result_t tvg_parse_fill_rectangles(tvg_context_t *ctx, + size_t size, + const tvg_style_t *fill_style) +{ + size_t count = size; + tvg_result_t res; + tvg_rect_t r; + twin_path_t *path = ctx->path; + while (count--) { + res = tvg_parse_rect(ctx, &r); + __return_val_if_fail(res, "Failed to parse rect"); + twin_path_rectangle(path, D(r.x), D(r.y), D(r.width), D(r.height)); + _fill_path_with_style(ctx, fill_style); + twin_path_empty(path); + } + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_line_fill_rectangles( + tvg_context_t *ctx, + size_t size, + const tvg_style_t *fill_style, + const tvg_style_t *line_style, + float line_width) +{ + size_t count = size; + tvg_result_t res; + tvg_rect_t r; + if (line_width == 0) { + line_width = .01; + } + twin_path_t *path = ctx->path; + while (count--) { + res = tvg_parse_rect(ctx, &r); + __return_val_if_fail(res, "Failed to parse rect"); + + twin_path_rectangle(path, D(r.x), D(r.y), D(r.width), D(r.height)); + _fill_path_with_style(ctx, fill_style); + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); + } + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_fill_paths(tvg_context_t *ctx, + size_t size, + const tvg_style_t *style) +{ + tvg_result_t res = TVG_SUCCESS; + uint32_t *sizes = malloc(size * sizeof(uint32_t)); + if (!sizes) { + return TVG_E_OUT_OF_MEMORY; + } + for (size_t i = 0; i < size; ++i) { + res = tvg_read_varuint(ctx, &sizes[i]); + ++sizes[i]; + __goto_if_fail(res, error, "Failed to read varuint"); + } + twin_path_t *path = ctx->path; + /* parse path */ + for (size_t i = 0; i < size; ++i) { + res = tvg_parse_path(ctx, sizes[i]); + __goto_if_fail(res, error, "Failed to parse path"); + } + _fill_path_with_style(ctx, style); + twin_path_empty(path); +error: + free(sizes); + return res; +} + +static tvg_result_t tvg_parse_line_paths(tvg_context_t *ctx, + size_t size, + const tvg_style_t *line_style, + float line_width) +{ + tvg_result_t res = TVG_SUCCESS; + uint32_t *sizes = malloc(size * sizeof(uint32_t)); + if (!sizes) { + return TVG_E_OUT_OF_MEMORY; + } + twin_path_t *path = ctx->path; + for (size_t i = 0; i < size; ++i) { + res = tvg_read_varuint(ctx, &sizes[i]); + ++sizes[i]; + __goto_if_fail(res, error, "Failed to read varuint"); + } + /* parse path */ + for (size_t i = 0; i < size; ++i) { + res = tvg_parse_path(ctx, sizes[i]); + __goto_if_fail(res, error, "Failed to parse path"); + } + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); +error: + free(sizes); + return res; +} + +static tvg_result_t tvg_parse_line_fill_paths(tvg_context_t *ctx, + size_t size, + const tvg_style_t *fill_style, + const tvg_style_t *line_style, + float line_width) +{ + tvg_result_t res = TVG_SUCCESS; + uint32_t *sizes = malloc(size * sizeof(uint32_t)); + if (!sizes) { + return TVG_E_OUT_OF_MEMORY; + } + for (size_t i = 0; i < size; ++i) { + res = tvg_read_varuint(ctx, &sizes[i]); + ++sizes[i]; + __goto_if_fail(res, error, "Failed to read varuint"); + } + twin_path_t *path = ctx->path; + + /* parse path */ + for (size_t i = 0; i < size; ++i) { + res = tvg_parse_path(ctx, sizes[i]); + __goto_if_fail(res, error, "Failed to parse path"); + } + if (line_width == 0) { + line_width = .1; + } + _fill_path_with_style(ctx, fill_style); + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); +error: + free(sizes); + return res; +} + +static tvg_result_t tvg_parse_fill_polygon(tvg_context_t *ctx, + size_t size, + const tvg_style_t *fill_style) +{ + size_t count = size; + tvg_point_t pt; + tvg_result_t res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_t *path = ctx->path; + twin_path_move(path, D(pt.x), D(pt.y)); + while (--count) { + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_draw(path, D(pt.x), D(pt.y)); + } + twin_path_close(path); + _fill_path_with_style(ctx, fill_style); + twin_path_empty(path); + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_polyline(tvg_context_t *ctx, + size_t size, + const tvg_style_t *line_style, + float line_width, + bool close) +{ + tvg_point_t pt; + tvg_result_t res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_t *path = ctx->path; + twin_path_move(path, D(pt.x), D(pt.y)); + for (size_t i = 1; i < size; ++i) { + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_draw(path, D(pt.x), D(pt.y)); + } + if (close) { + twin_path_close(path); + } + if (line_width == 0) { + line_width = .01; + } + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_line_fill_polyline(tvg_context_t *ctx, + size_t size, + const tvg_style_t *fill_style, + const tvg_style_t *line_style, + float line_width, + bool close) +{ + tvg_point_t pt; + tvg_result_t res = tvg_read_point(ctx, &pt); + twin_path_t *path = ctx->path; + twin_path_move(path, D(pt.x), D(pt.y)); + for (size_t i = 1; i < size; ++i) { + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + } + if (close) { + twin_path_close(path); + } + _fill_path_with_style(ctx, fill_style); + if (line_width == 0) { + line_width = .01; + } + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); + return res; +} + +static tvg_result_t tvg_parse_lines(tvg_context_t *ctx, + size_t size, + const tvg_style_t *line_style, + float line_width) +{ + tvg_point_t pt; + tvg_result_t res; + twin_path_t *path = ctx->path; + for (size_t i = 0; i < size; ++i) { + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_move(path, D(pt.x), D(pt.y)); + res = tvg_read_point(ctx, &pt); + __return_val_if_fail(res, "Failed to read point"); + twin_path_draw(path, D(pt.x), D(pt.y)); + } + if (line_width == 0) { + line_width = .01; + } + _stroke_path_with_style(ctx, line_style, D(line_width)); + twin_path_empty(path); + return TVG_SUCCESS; +} + +static tvg_result_t tvg_parse_commands(tvg_context_t *ctx) +{ + tvg_result_t res = TVG_SUCCESS; + uint8_t cmd = 255; + size_t read = 0; + while (cmd != 0) { + READ_VALUE(read, &cmd, 1); + switch (TVG_CMD_INDEX(cmd)) { + case TVG_CMD_END_DOCUMENT: + break; + case TVG_CMD_FILL_POLYGON: { + tvg_fill_header_t data; + res = tvg_parse_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse fill header"); + res = tvg_parse_fill_polygon(ctx, data.size, &data.style); + __return_val_if_fail(res, "Failed to parse polygon"); + } break; + case TVG_CMD_FILL_RECTANGLES: { + tvg_fill_header_t data; + res = tvg_parse_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse fill header"); + res = tvg_parse_fill_rectangles(ctx, data.size, &data.style); + __return_val_if_fail(res, "Failed to parse rectangles"); + } break; + case TVG_CMD_FILL_PATH: { + tvg_fill_header_t data; + res = tvg_parse_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse fill header"); + res = tvg_parse_fill_paths(ctx, data.size, &data.style); + __return_val_if_fail(res, "Failed to parse paths"); + } break; + case TVG_CMD_DRAW_LINES: { + tvg_line_header_t data; + res = tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line header"); + res = tvg_parse_lines(ctx, data.size, &data.style, data.line_width); + __return_val_if_fail(res, "Failed to parse lines"); + } break; + case TVG_CMD_DRAW_LINE_LOOP: { + tvg_line_header_t data; + res = tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line header"); + res = tvg_parse_polyline(ctx, data.size, &data.style, + data.line_width, true); + __return_val_if_fail(res, "Failed to parse polylines"); + } break; + case TVG_CMD_DRAW_LINE_STRIP: { + tvg_line_header_t data; + res = tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line header"); + res = tvg_parse_polyline(ctx, data.size, &data.style, + data.line_width, false); + __return_val_if_fail(res, "Failed to parse polylines"); + } break; + case TVG_CMD_DRAW_LINE_PATH: { + tvg_line_header_t data; + res = tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line header"); + res = tvg_parse_line_paths(ctx, data.size, &data.style, + data.line_width); + __return_val_if_fail(res, "Failed to parse paths"); + } break; + case TVG_CMD_OUTLINE_FILL_POLYGON: { + tvg_line_fill_header_t data; + res = + tvg_parse_line_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line and fill header"); + res = tvg_parse_line_fill_polyline(ctx, data.size, &data.fill_style, + &data.line_style, + data.line_width, true); + __return_val_if_fail(res, "Failed to parse polyline"); + } break; + case TVG_CMD_OUTLINE_FILL_RECTANGLES: { + tvg_line_fill_header_t data; + res = + tvg_parse_line_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line and fill header"); + res = tvg_parse_line_fill_rectangles( + ctx, data.size, &data.fill_style, &data.line_style, + data.line_width); + __return_val_if_fail(res, "Failed to parse rectangles"); + } break; + case TVG_CMD_OUTLINE_FILL_PATH: { + tvg_line_fill_header_t data; + res = + tvg_parse_line_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data); + __return_val_if_fail(res, "Failed to parse line and fill header"); + res = tvg_parse_line_fill_paths(ctx, data.size, &data.fill_style, + &data.line_style, data.line_width); + __return_val_if_fail(res, "Failed to parse paths"); + } break; + default: + return TVG_E_INVALID_FORMAT; + } + } + return TVG_SUCCESS; +} + +static tvg_result_t tvg_document_dimensions(tvg_input_func_t in, + void *in_state, + uint32_t *out_width, + uint32_t *out_height) +{ + /* initialize the context */ + tvg_context_t ctx; + if (!in) { + return TVG_E_INVALID_ARG; + } + ctx.in = in; + ctx.in_state = in_state; + ctx.colors = NULL; + ctx.colors_size = 0; + /* parse the header, early outing before the color table */ + tvg_result_t res = tvg_parse_header(&ctx, 1); + __return_val_if_fail(res, "Failed to parse header"); + *out_width = ctx.width; + *out_height = ctx.height; + return res; +} + +static tvg_result_t tvg_render_document(tvg_input_func_t in, + void *in_state, + twin_pixmap_t *pix, + twin_fixed_t width_scale, + twin_fixed_t height_scale) +{ + /* initialize the context */ + tvg_context_t ctx; + if (!in) { + return TVG_E_INVALID_ARG; + } + ctx.in = in; + ctx.in_state = in_state; + ctx.pixmap = pix; + ctx.colors = NULL; + ctx.colors_size = 0; + /* parse the header */ + tvg_result_t res = tvg_parse_header(&ctx, 0); + __goto_if_fail(res, error, "Failed to parse header"); + ctx.path = twin_path_create(); + if (!ctx.path) { + log_error("Failed to create path"); + goto error; + } + twin_path_scale(ctx.path, width_scale, height_scale); + res = tvg_parse_commands(&ctx); + __goto_if_fail(res, error, "Failed to parse commands"); +error: + if (ctx.path) { + twin_path_destroy(ctx.path); + ctx.path = NULL; + } + if (ctx.colors) { + free(ctx.colors); + ctx.colors = NULL; + ctx.colors_size = 0; + } + return res; +} + +static size_t inp_func(uint8_t *data, size_t to_read, void *state) +{ + FILE *f = (FILE *) state; + return fread(data, 1, to_read, f); +} + +twin_pixmap_t *_twin_tvg_to_pixmap(const char *filepath, twin_format_t fmt) +{ + FILE *infile = NULL; + twin_pixmap_t *pix = NULL; + uint32_t width, height; + tvg_result_t res; + + /* Current implementation only produces TWIN_ARGB32 */ + if (fmt != TWIN_ARGB32) { + log_error("Unsupported color format"); + goto bail; + } + + if (!filepath) { + log_error("Invalid filepath"); + goto bail; + } + + infile = fopen(filepath, "rb"); + if (!infile) { + log_error("Failed to open %s", filepath); + goto bail; + } + + res = tvg_document_dimensions(inp_func, infile, &width, &height); + if (res != TVG_SUCCESS) { + log_error("Failed to get document dimensions"); + goto bail_infile; + } + + if (fseek(infile, 0, SEEK_SET) != 0) { + log_error("Failed to seek file"); + goto bail_infile; + } + + pix = twin_pixmap_create(fmt, width, height); + if (!pix) { + log_error("Failed to create pixmap"); + goto bail_infile; + } + + res = tvg_render_document(inp_func, infile, pix, TWIN_FIXED_ONE, + TWIN_FIXED_ONE); + if (res != TVG_SUCCESS) { + log_error("Failed to render document"); + goto bail_pixmap; + } + + fclose(infile); + return pix; + +bail_pixmap: + twin_pixmap_destroy(pix); +bail_infile: + fclose(infile); +bail: + return NULL; +} + +twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath, + twin_format_t fmt, + twin_coord_t w, + twin_coord_t h) +{ + FILE *infile = NULL; + twin_pixmap_t *pix = NULL; + uint32_t width, height; + tvg_result_t res; + + /* Current implementation only produces TWIN_ARGB32 */ + if (fmt != TWIN_ARGB32) { + log_error("Unsupported color format"); + goto bail; + } + + if (!filepath) { + log_error("Invalid filepath"); + goto bail; + } + + infile = fopen(filepath, "rb"); + if (!infile) { + log_error("Failed to open %s", filepath); + goto bail; + } + + res = tvg_document_dimensions(inp_func, infile, &width, &height); + __goto_if_fail(res, bail_infile, "Failed to get document dimensions"); + + if (fseek(infile, 0, SEEK_SET) != 0) { + log_error("Failed to seek file"); + goto bail_infile; + } + + pix = twin_pixmap_create(fmt, w, h); + if (!pix) { + log_error("Failed to create pixmap"); + goto bail_infile; + } + twin_fixed_t width_scale = D((double) w / (double) width); + twin_fixed_t height_scale = D((double) h / (double) height); + twin_fixed_t scale = MIN(width_scale, height_scale); + res = tvg_render_document(inp_func, infile, pix, scale, scale); + __goto_if_fail(res, bail_pixmap, "Failed to render document"); + + fclose(infile); + return pix; + +bail_pixmap: + twin_pixmap_destroy(pix); +bail_infile: + fclose(infile); +bail: + return NULL; +} diff --git a/src/image.c b/src/image.c index f91c194..21829e0 100644 --- a/src/image.c +++ b/src/image.c @@ -21,6 +21,10 @@ #define CONFIG_LOADER_GIF 0 #endif +#if !defined(CONFIG_LOADER_TVG) +#define CONFIG_LOADER_TVG 0 +#endif + /* Feature test macro */ #define LOADER_HAS(x) CONFIG_LOADER_##x @@ -34,6 +38,9 @@ ) \ IIF(LOADER_HAS(GIF))( \ _(gif) \ + ) \ + IIF(LOADER_HAS(TVG))( \ + _(tvg) \ ) /* clang-format on */ @@ -54,12 +61,15 @@ typedef enum { * https://www.file-recovery.com/jpg-signature-format.htm * - GIF: * https://www.file-recovery.com/gif-signature-format.htm + * - TinyVG: + * https://tinyvg.tech/download/specification.pdf */ static const uint8_t header_png[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, }; static const uint8_t header_jpeg[3] = {0xFF, 0xD8, 0xFF}; static const uint8_t header_gif[4] = {0x47, 0x49, 0x46, 0x38}; +static const uint8_t header_tvg[2] = {0x72, 0x56}; static twin_image_format_t image_type_detect(const char *path) { @@ -91,6 +101,11 @@ static twin_image_format_t image_type_detect(const char *path) type = IMAGE_TYPE_gif; } #endif +#if LOADER_HAS(TVG) + else if (!memcmp(header, header_tvg, sizeof(header_tvg))) { + type = IMAGE_TYPE_tvg; + } +#endif /* otherwise, unsupported format */ return type; diff --git a/src/trig.c b/src/trig.c index 3b463f4..9a47ca5 100644 --- a/src/trig.c +++ b/src/trig.c @@ -99,18 +99,21 @@ void twin_sincos(twin_angle_t a, twin_fixed_t *sin, twin_fixed_t *cos) } static const twin_angle_t atan_table[] = { - 0x0200, /* arctan(2^0) = 45° -> 512 */ - 0x012E, /* arctan(2^-1) = 26.565° -> 302 */ - 0x00A0, /* arctan(2^-2) = 14.036° -> 160 */ - 0x0051, /* arctan(2^-3) = 7.125° -> 81 */ - 0x0029, /* arctan(2^-4) = 3.576° -> 41 */ - 0x0014, /* arctan(2^-5) = 1.790° -> 20 */ - 0x000A, /* arctan(2^-6) = 0.895° -> 10 */ - 0x0005, /* arctan(2^-7) = 0.448° -> 5 */ - 0x0003, /* arctan(2^-8) = 0.224° -> 3 */ - 0x0001, /* arctan(2^-9) = 0.112° -> 1 */ - 0x0001, /* arctan(2^-10) = 0.056° -> 1 */ - 0x0000, /* arctan(2^-11) = 0.028° -> 0 */ + 0x1000, /* arctan(2^0) = 45° -> 4096 */ + 0x0972, /* arctan(2^-1) = 26.565° -> 2418 */ + 0x04fe, /* arctan(2^-2) = 14.036° -> 1278 */ + 0x0289, /* arctan(2^-3) = 7.125° -> 649 */ + 0x0146, /* arctan(2^-4) = 3.576° -> 326 */ + 0x00a3, /* arctan(2^-5) = 1.790° -> 163 */ + 0x0051, /* arctan(2^-6) = 0.895° -> 81 */ + 0x0029, /* arctan(2^-7) = 0.448° -> 41 */ + 0x0014, /* arctan(2^-8) = 0.224° -> 20 */ + 0x000a, /* arctan(2^-9) = 0.112° -> 10 */ + 0x0005, /* arctan(2^-10) = 0.056° -> 5 */ + 0x0003, /* arctan(2^-11) = 0.028° -> 3 */ + 0x0001, /* arctan(2^-12) = 0.014° -> 1 */ + 0x0001, /* arctan(2^-13) = 0.007° -> 1 */ + 0x0000, /* arctan(2^-14) = 0.0035° -> 0 */ }; static twin_angle_t twin_atan2_first_quadrant(twin_fixed_t y, twin_fixed_t x) @@ -121,9 +124,14 @@ static twin_angle_t twin_atan2_first_quadrant(twin_fixed_t y, twin_fixed_t x) return TWIN_ANGLE_90; if (y == 0) return TWIN_ANGLE_0; - twin_angle_t angle = TWIN_ANGLE_0; + twin_angle_t angle = 0; /* CORDIC iteration */ - for (int i = 0; i < 12; i++) { + /* + * To enhance accuracy, the angle is mapped from the range 0-360 degrees to + * 0-32768. Allows for finer resolution to additional CORDIC iterations for + * more precise calculations. + */ + for (int i = 0; i < 15; i++) { twin_fixed_t temp_x = x; if (y > 0) { x += (y >> i); @@ -135,7 +143,7 @@ static twin_angle_t twin_atan2_first_quadrant(twin_fixed_t y, twin_fixed_t x) angle -= atan_table[i]; } } - return angle; + return (twin_angle_t) (double) angle / (32768.0) * TWIN_ANGLE_360; } twin_angle_t twin_atan2(twin_fixed_t y, twin_fixed_t x)