We're going to use the open source plutovg offering to render TinyVG files to PNG. In the process we'll explore how TinyVG works.
Introduction
I encourage you to start here at this link. You'll find a website with an overview of TinyVG, a compact binary format for representing vector graphics.
TinyVG aims to take some of the bloat and pain out of dealing with vector graphics formats like SVG. It is much simpler to parse than SVG, requiring about a 3rd of the code compared to my basic SVG parser for embedded.
I should be very clear that I did not create TinyVG. It was created by Felix Queißner (@ikskuh on Github - xq on the TinyVG Discord server), I did however, create this implementation with some guidance from him.
I created this implementation because I figured it would help TinyVG reach more folks than the Zig reference implementation already provided, and am presenting it here for your use in your own software.
I chose simple C for the implementation because it is essentially a reference implementation and C is a good language to port away from as long as the original code isn't crazy. It should be fairly readable even if you aren't a C expert.
You should be able to build and execute this on any 32-bit PC or better, using MSVC, GCC, or (probably) Clang. You can run it by downloading it and opening it with VS Code using the CMake tools extension.
Rather than require you to fiddle with the CMake settings to change command line arguments, I simply hard coded the input, outputs, and scale factor in main()
.
Background
TinyVG's TVG format is a binary format. It does a lot of bit twiddling to pack as much information in as little space as possible which can make reading it a bit fiddly at first. Fortunately, the format is simple overall so this isn't a huge concern.
We'll be using PlutoVG to render the output. PlutoVG seems as though it was designed to render SVG because its APIs parameters are similar to what SVG has, so it's a pretty direct mapping from SVG to PlutoVG. It's also got a relatively simple API overall so I felt it was a good candidate for rendering the output. You may be able to sort of glean the relationship between SVG and TVG formats by looking at this code, as I did.
Implementing This Mess
Here we'll explore main.c which contains all the relevant bits. Starting at the top:
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <plutovg.h> // renderer
Most of these are fairly standard except plutovg.h which is our renderer backend.
Next we have several anonymous enumerations.
The first one is the set of commands, which you'll find starting on page 6 of the specification.
enum {
TVG_CMD_END_DOCUMENT = 0,
TVG_CMD_FILL_POLYGON,
TVG_CMD_FILL_RECTANGLES,
TVG_CMD_FILL_PATH,
TVG_CMD_DRAW_LINES,
TVG_CMD_DRAW_LINE_LOOP,
TVG_CMD_DRAW_LINE_STRIP,
TVG_CMD_DRAW_LINE_PATH,
TVG_CMD_OUTLINE_FILL_POLYGON,
TVG_CMD_OUTLINE_FILL_RECTANGLES,
TVG_CMD_OUTLINE_FILL_PATH
};
Next is our paint styles, which can be a solid color or one of two types of gradients:
enum {
TVG_STYLE_FLAT = 0,
TVG_STYLE_LINEAR,
TVG_STYLE_RADIAL
};
Now we have the unit size. Units are fixed point real numbers. This is one of those areas where the TVG format goes out of its way to save space. Basically, it uses the smallest possible values to represent a value in coordinate space. The enumeration below indicates of the total width:
enum {
TVG_RANGE_DEFAULT = 0,
TVG_RANGE_REDUCED,
TVG_RANGE_ENHANCED,
};
Onward, to the color encodings. They're described in detail in the comments:
enum {
TVG_COLOR_U8888 = 0,
TVG_COLOR_U565,
TVG_COLOR_F32,
TVG_COLOR_CUSTOM,
};
I alluded to the use of fixed point numbers above. The next enumeration covers the scaling factor for the fixed point units:
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,
};
Onto path commands. You should note that each of these commands has a corresponding command in the d
attribute on SVG <path>
elements:
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
};
Finally several error codes:
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
};
Now we've got macros! We use these mostly to hide ugly bit shifts and to hold some much needed constant values:
#define TVG_PI (3.1415926536f)
#define TVG_CLAMP(x,mn,mx) (x>mx?mx:(x<mn?mn:x))
#define TVG_RGB16_R(x) (x & 0x1F)
#define TVG_RGB16_G(x) ((x>>5) & 0x3F)
#define TVG_RGB16_B(x) ((x>>11) & 0x1F)
#define TVG_CMD_INDEX(x) (x&0x3F)
#define TVG_CMD_STYLE_KIND(x) ((x>>6)&0x3)
#define TVG_SIZE_AND_STYLE_SIZE(x) ((x&0x3F)+1)
#define TVG_SIZE_AND_STYLE_STYLE_KIND(x) ((x>>6)&0x3)
#define TVG_HEADER_DATA_SCALE(x) (x&0x0F)
#define TVG_HEADER_DATA_COLOR_ENC(x) ((x>>4)&0x03)
#define TVG_HEADER_DATA_RANGE(x) ((x>>6)&0x03)
#define TVG_PATH_CMD_INDEX(x) (x&0x7)
#define TVG_PATH_CMD_HAS_LINE(x) ((x>>4)&0x1)
#define TVG_ARC_LARGE(x) (x&0x1)
#define TVG_ARC_SWEEP(x) ((x>>1)&1)
Now some basic datatype structures. They're pretty simple and self explanatory:
typedef struct {
float r, g, b, a;
} tvg_f32_pixel_t;
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
} tvg_rgba32_t;
typedef struct {
float r;
float g;
float b;
} tvg_rgb_t;
typedef struct {
float r;
float g;
float b;
float a;
} tvg_rgba_t;
typedef struct {
float x;
float y;
} tvg_point_t;
typedef struct {
float x;
float y;
float width;
float height;
} tvg_rect_t;
Now we have the gradient structure. Gradients in TVG can be radial or linear, but are otherwise quite simple:
typedef struct {
tvg_point_t point0;
tvg_point_t point1;
uint32_t color0;
uint32_t color1;
} tvg_gradient_t;
It consists of two points, and two colors. In the case of the radial data, the 2nd point isn't fully utilized - it's simply used to compute the distance from point0
in order to get the radius. I'll cover it more when we use it.
Next we have the style data. This consists of a kind field indicating either solid color, linear gradient, or radial gradient, and then a union containing each:
typedef struct {
uint8_t kind;
union {
uint32_t flat; tvg_gradient_t linear;
tvg_gradient_t radial;
};
} tvg_style_t;
The following three structs define one of three types of headers depending on the command:
typedef struct {
tvg_style_t style;
size_t size;
} tvg_fill_header_t;
typedef struct {
tvg_style_t style;
float line_width;
size_t size;
} tvg_line_header_t;
typedef struct {
tvg_style_t fill_style;
tvg_style_t line_style;
float line_width;
size_t size;
} tvg_line_fill_header_t;
The first one is when the object has no "SVG stroke", only an "SVG fill". The second one is for when there is only a stroke, and the final one is for both stroke and fill. I'm using SVG analogies here so you can have a frame of reference.
Now we have the input function. This provides a cursor over an arbitrary data source such as a file or an array of bytes. The implementation therein provides an implementation for the <stdio.h> FILE
object.
typedef size_t (*tvg_input_func_t)
(uint8_t* data, size_t size, void* state);
Now we have the context struct used for parsing the file. It's a bookkeeping structure we keep around during the parse and render process:
typedef struct {
tvg_input_func_t inp;
void* inp_state;
plutovg_canvas_t* canvas;
uint8_t scale;
uint8_t color_encoding;
uint8_t coord_range;
uint32_t width, height;
size_t colors_size;
tvg_rgba_t* colors;
} tvg_context_t;
The next typedef
just defines the return type of our user facing API functions. It's either 0 (TVG_SUCCESS
) or one of the TVG_E_
error codes:
typedef int tvg_result_t;
The next function resolves a peculiarity with the way TVG handles zero values. Since in some cases zero doesn't make sense, such as with the document width and height, any zero value is instead mapped to the maximum possible value:
static uint32_t tvg_map_zero_to_max(tvg_context_t* ctx, uint32_t value) {
if (0 == value) {
switch (ctx->coord_range) {
case TVG_RANGE_DEFAULT:
return 0xFFFF;
case TVG_RANGE_REDUCED:
return 0xFF;
default:
return 0xFFFFFFFF;
}
}
return value;
}
The following code reads a coordinate and returns the unscaled raw value. It's more of a helper function than anything, since the raw value by itself isn't very useful until scaled:
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 = ctx->inp((uint8_t*)&u16, sizeof(uint16_t), ctx->inp_state);
if (sizeof(uint16_t) > read) {
return TVG_E_IO_ERROR;
}
*out_raw_value = u16;
return TVG_SUCCESS;
}
case TVG_RANGE_REDUCED: {
uint8_t u8;
read = ctx->inp((uint8_t*)&u8, sizeof(uint8_t), ctx->inp_state);
if (sizeof(uint8_t) > read) {
return TVG_E_IO_ERROR;
}
*out_raw_value = u8;
return TVG_SUCCESS;
}
default:
read = ctx->inp((uint8_t*)out_raw_value, sizeof(uint32_t),
ctx->inp_state);
if (sizeof(uint32_t) > read) {
return TVG_E_IO_ERROR;
}
return TVG_SUCCESS;
}
}
The next function reads a color out of the file, converting from the file's stored format, to a tvg_rgba_t
runtime representation:
static tvg_result_t tvg_read_color(tvg_context_t* ctx, tvg_rgba_t* out_color) {
size_t read;
switch (ctx->color_encoding) {
case TVG_COLOR_F32: {
tvg_f32_pixel_t data;
read = ctx->inp((uint8_t*)&data, sizeof(data), ctx->inp_state);
if (sizeof(data) > read) {
return TVG_E_IO_ERROR;
}
out_color->r = data.r;
out_color->g = data.g;
out_color->b = data.b;
out_color->a = data.a;
return TVG_SUCCESS;
}
case TVG_COLOR_U565: {
uint16_t data;
read = ctx->inp((uint8_t*)&data, sizeof(data), ctx->inp_state);
if (sizeof(data) > read) {
return TVG_E_IO_ERROR;
}
out_color->r = ((float)TVG_RGB16_R(data)) / 15.0f;
out_color->g = ((float)TVG_RGB16_G(data)) / 31.0f;
out_color->b = ((float)TVG_RGB16_B(data)) / 15.0f;
out_color->a = 1.0f;
return TVG_SUCCESS;
}
case TVG_COLOR_U8888: {
tvg_rgba32_t data;
read = ctx->inp((uint8_t*)&data.r, 1, ctx->inp_state);
if (1 > read) {
return TVG_E_IO_ERROR;
}
read = ctx->inp((uint8_t*)&data.g, 1, ctx->inp_state);
if (1 > read) {
return TVG_E_IO_ERROR;
}
read = ctx->inp((uint8_t*)&data.b, 1, ctx->inp_state);
if (1 > read) {
return TVG_E_IO_ERROR;
}
read = ctx->inp((uint8_t*)&data.a, 1, ctx->inp_state);
if (1 > read) {
return TVG_E_IO_ERROR;
}
out_color->r = ((float)data.r) / 255.0f;
out_color->g = ((float)data.g) / 255.0f;
out_color->b = ((float)data.b) / 255.0f;
out_color->a = ((float)data.a) / 255.0f;
return TVG_SUCCESS;
}
case TVG_COLOR_CUSTOM:
return TVG_E_NOT_SUPPORTED;
default:
return TVG_E_INVALID_FORMAT;
}
}
The next function handles coordinate scaling. This is also basically just an internal helper function:
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;
}
The next function deals with the fact that TVG packs integers such that a uint32_t
can mostly be represented using a varying number of (up to 4) bytes. The specification has details, including an implementation example:
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;
while (true) {
if (1 > ctx->inp(&byte, 1, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
const uint32_t val = ((uint32_t)(byte & 0x7F)) << (7 * count);
result |= val;
if ((byte & 0x80) == 0) break;
++count;
}
*out_value = result;
return TVG_SUCCESS;
}
Now we have a crucial function that reads a coordinate unit out of the file, and appropriately scales it:
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);
if (res != TVG_SUCCESS) {
return res;
}
*out_value = tvg_downscale_coord(ctx, val);
return TVG_SUCCESS;
}
Another often used function follows. It reads a 2D point out of the file:
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);
if (res != TVG_SUCCESS) {
return res;
}
out_point->x = f32;
res = tvg_read_unit(ctx, &f32);
if (res != TVG_SUCCESS) {
return res;
}
out_point->y = f32;
return TVG_SUCCESS;
}
Now onto something a bit more meaty - parsing the header at the beginning of the file:
static tvg_result_t tvg_parse_header(tvg_context_t* ctx, int dim_only) {
uint8_t data[2];
if (2 > ctx->inp((uint8_t*)data, 2, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
if (data[0] != 0x72 || data[1] != 0x56) {
return TVG_E_INVALID_FORMAT;
}
if (1 > ctx->inp(data, 1, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
if (data[0] != 1) {
return TVG_E_NOT_SUPPORTED;
}
if (1 > ctx->inp(data, 1, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
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]);
uint32_t tmp;
tvg_result_t res = tvg_read_coord(ctx, &tmp);
if (res != TVG_SUCCESS) {
return res;
}
ctx->width = tvg_map_zero_to_max(ctx, tmp);
res = tvg_read_coord(ctx, &tmp);
if (res != TVG_SUCCESS) {
return res;
}
ctx->height = tvg_map_zero_to_max(ctx, tmp);
if (dim_only) {
return TVG_SUCCESS;
}
uint32_t color_count;
res = tvg_read_varuint(ctx, &color_count);
if (res != TVG_SUCCESS) {
return res;
}
if (color_count == 0) {
return TVG_E_INVALID_FORMAT;
}
ctx->colors = (tvg_rgba_t*)malloc(color_count * sizeof(tvg_rgba_t));
if (ctx->colors == NULL) {
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;
}
Now onto parsing the gradient data - this is the same regardless of type of gradient:
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);
if (res != TVG_SUCCESS) {
return res;
}
out_gradient->point0 = pt;
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
return res;
}
out_gradient->point1 = pt;
res = tvg_read_varuint(ctx, &u32);
if (res != TVG_SUCCESS) {
return res;
}
out_gradient->color0 = u32;
if (u32 > ctx->colors_size) {
return TVG_E_INVALID_FORMAT;
}
res = tvg_read_varuint(ctx, &u32);
if (res != TVG_SUCCESS) {
return res;
}
if (u32 > ctx->colors_size) {
return TVG_E_INVALID_FORMAT;
}
out_gradient->color1 = u32;
return TVG_SUCCESS;
}
Now we parse a style, which can be a flat color - a lookup into the color table, or one of two types of gradients:
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);
if (res != TVG_SUCCESS) {
return res;
}
out_style->flat = flat;
break;
case TVG_STYLE_LINEAR:
res = tvg_parse_gradient(ctx, &grad);
out_style->linear = grad;
break;
case TVG_STYLE_RADIAL:
res = tvg_parse_gradient(ctx, &grad);
out_style->radial = grad;
break;
default:
res = TVG_E_INVALID_FORMAT;
break;
}
if (res != TVG_SUCCESS) {
return res;
}
return res;
}
The next function simply calculates the distance between two points:
static float tvg_distance(const tvg_point_t* lhs, const tvg_point_t* rhs) {
float xd = rhs->x - lhs->x;
float yd = rhs->y - lhs->y;
return sqrtf((xd * xd) + (yd * yd));
}
The following simply converts a TVG color representation into one suitable for PlutoVG:
static plutovg_color_t tvg_color_to_plutovg(const tvg_rgba_t* col) {
plutovg_color_t result;
plutovg_color_init_rgba(&result, col->r, col->g, col->b, col->a);
return result;
}
The next function takes style data and plugs it into PlutoVG. PlutoVG renders strokes and fills in separate passes, so which style is applied depends on whether we're doing stroke of fill:
static tvg_result_t tvg_apply_style(tvg_context_t* ctx, const tvg_style_t* style) {
plutovg_color_t col;
float r;
plutovg_gradient_stop_t stops[2];
switch (style->kind) {
case TVG_STYLE_FLAT:
col = tvg_color_to_plutovg(&ctx->colors[style->flat]);
plutovg_canvas_set_color(ctx->canvas, &col);
break;
case TVG_STYLE_LINEAR:
col = tvg_color_to_plutovg(&ctx->colors[style->linear.color0]);
stops[0].color = col;
stops[0].offset = 0;
col = tvg_color_to_plutovg(&ctx->colors[style->linear.color1]);
stops[1].color = col;
stops[1].offset = 1;
plutovg_canvas_set_linear_gradient(
ctx->canvas, style->linear.point0.x, style->linear.point0.y,
style->linear.point1.x, style->linear.point1.y,
PLUTOVG_SPREAD_METHOD_PAD, stops, 2, NULL);
break;
case TVG_STYLE_RADIAL:
col = tvg_color_to_plutovg(&ctx->colors[style->radial.color0]);
stops[0].color = col;
stops[0].offset = 0;
col = tvg_color_to_plutovg(&ctx->colors[style->radial.color1]);
stops[1].color = col;
stops[1].offset = 1;
r = tvg_distance(&style->radial.point0, &style->radial.point1);
plutovg_canvas_set_radial_gradient(
ctx->canvas, style->radial.point0.x, style->radial.point0.y, r,
style->radial.point1.x, style->radial.point1.y, r,
PLUTOVG_SPREAD_METHOD_REFLECT, stops, 2, NULL);
break;
default:
return TVG_E_INVALID_FORMAT;
}
return TVG_SUCCESS;
}
Note that gradients always have exactly two stops at 0% and 100%, making it more restrictive than SVG.
The next three functions parse the header information for the three different types of headers associated with the commands. The first one is fill only, the second one is stroke only, and the final is fill and stroke - again, I'm using SVG terminology here.
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);
if (res != TVG_SUCCESS) {
return res;
}
size_t count = (size_t)u32 + 1;
out_header->size = count;
res = tvg_parse_style(ctx, kind, &out_header->style);
if (res != TVG_SUCCESS) {
return res;
}
return TVG_SUCCESS;
}
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);
if (res != TVG_SUCCESS) {
return res;
}
size_t count = (size_t)u32 + 1;
out_header->size = count;
res = tvg_parse_style(ctx, kind, &out_header->style);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_read_unit(ctx, &out_header->line_width);
if (res != TVG_SUCCESS) {
return res;
}
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) {
uint32_t u32;
uint8_t d;
if (1 > ctx->inp(&d, 1, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
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);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_style(ctx, TVG_SIZE_AND_STYLE_STYLE_KIND(d),
&out_header->line_style);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_read_unit(ctx, &out_header->line_width);
if (res != TVG_SUCCESS) {
return res;
}
return TVG_SUCCESS;
}
The next function parses path commands out of the file. Keep in mind this is very similar to the d
attribute on the <path>
attribute in SVG. All of the path commands in TVG are almost or entirely directly mapped to SVG equivalents. Two minor differences are the initial "move to" command is not treated like a command, but is simply represented by an initial point indicating the starting location of the path, and the other difference is that the sweep flag on "arc to" is inverted compared to SVG:
static tvg_result_t tvg_parse_path(tvg_context_t* ctx, size_t size) {
tvg_result_t res = TVG_SUCCESS;
tvg_point_t st, cur;
tvg_point_t pt;
uint32_t u32;
float f32;
uint8_t d;
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_move_to(ctx->canvas, pt.x, pt.y);
st = pt;
cur = pt;
for (size_t j = 0; j < size; ++j) {
if (1 > ctx->inp(&d, 1, ctx->inp_state)) {
goto error;
}
float line_width = 0.0f;
if (TVG_PATH_CMD_HAS_LINE(d)) {
res = tvg_read_unit(ctx, &line_width);
if (res != TVG_SUCCESS) {
goto error;
}
}
switch (TVG_PATH_CMD_INDEX(d)) {
case TVG_PATH_LINE:
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
cur = pt;
break;
case TVG_PATH_HLINE:
res = tvg_read_unit(ctx, &f32);
if (res != TVG_SUCCESS) {
goto error;
}
pt.x = f32;
pt.y = cur.y;
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
cur = pt;
break;
case TVG_PATH_VLINE:
res = tvg_read_unit(ctx, &f32);
if (res != TVG_SUCCESS) {
goto error;
}
pt.x = cur.x;
pt.y = (float)f32;
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
cur = pt;
break;
case TVG_PATH_CUBIC: {
tvg_point_t ctrl1, ctrl2, endp;
res = tvg_read_point(ctx, &ctrl1);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_point(ctx, &ctrl2);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_point(ctx, &endp);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_cubic_to(ctx->canvas, ctrl1.x, ctrl1.y, ctrl2.x,
ctrl2.y, endp.x, endp.y);
cur = endp;
} break;
case TVG_PATH_ARC_CIRCLE: {
uint8_t d;
if (1 > ctx->inp(&d, 1, ctx->inp_state)) {
res = TVG_E_IO_ERROR;
goto error;
}
float radius;
res = tvg_read_unit(ctx, &radius);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_arc_to(ctx->canvas, radius, radius, 0,
TVG_ARC_LARGE(d), 1 - TVG_ARC_SWEEP(d),
pt.x, pt.y);
cur = pt;
} break;
case TVG_PATH_ARC_ELLIPSE: {
uint8_t d;
if (1 > ctx->inp(&d, 1, ctx->inp_state)) {
res = TVG_E_IO_ERROR;
goto error;
}
float radius_x, radius_y;
float rotation;
res = tvg_read_unit(ctx, &radius_x);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_unit(ctx, &radius_y);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_unit(ctx, &rotation);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_arc_to(
ctx->canvas, radius_x, radius_y, rotation * (TVG_PI / 180.0f),
TVG_ARC_LARGE(d), 1 - TVG_ARC_SWEEP(d), pt.x, pt.y);
cur = pt;
} break;
case TVG_PATH_CLOSE:
plutovg_canvas_close_path(ctx->canvas);
cur = st;
break;
case TVG_PATH_QUAD: {
tvg_point_t ctrl, endp;
res = tvg_read_point(ctx, &ctrl);
if (res != TVG_SUCCESS) {
goto error;
}
res = tvg_read_point(ctx, &endp);
if (res != TVG_SUCCESS) {
goto error;
}
plutovg_canvas_quad_to(ctx->canvas, ctrl.x, ctrl.y, endp.x,
endp.y);
cur = endp;
} break;
default:
res = TVG_E_INVALID_FORMAT;
goto error;
}
}
error:
return res;
}
The next function parses a tvg_rect_t
rectangle out of the file:
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);
if (res != TVG_SUCCESS) return res;
float w, h;
res = tvg_read_unit(ctx, &w);
if (res != TVG_SUCCESS) return res;
res = tvg_read_unit(ctx, &h);
if (res != TVG_SUCCESS) return res;
out_rect->x = pt.x;
out_rect->y = pt.y;
out_rect->width = w;
out_rect->height = h;
return TVG_SUCCESS;
}
The following code handles fulfilling the rectangle based commands:
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;
size_t szb = count * sizeof(tvg_rect_t);
tvg_result_t res;
tvg_rect_t r;
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
res = tvg_apply_style(ctx, fill_style);
if (res != TVG_SUCCESS) return res;
while (count--) {
res = tvg_parse_rect(ctx,&r);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_rect(ctx->canvas, r.x, r.y, r.width, r.height);
plutovg_canvas_fill(ctx->canvas);
}
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;
size_t szb = count * sizeof(tvg_rect_t);
tvg_result_t res;
tvg_rect_t r;
if (line_width == 0) { line_width = .001;
}
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
while (count--) {
res = tvg_parse_rect(ctx,&r);
if (res != TVG_SUCCESS) return res;
res = tvg_apply_style(ctx, fill_style);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_rect(ctx->canvas, r.x, r.y, r.width, r.height);
plutovg_canvas_fill_preserve(ctx->canvas);
plutovg_canvas_set_line_width(ctx->canvas, line_width);
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_stroke(ctx->canvas);
}
return TVG_SUCCESS;
}
The next three functions handle the path based commands:
With paths there come a series of sizes that need to be read out of the file beforehand. Each one is incremented by one since 0 is invalid. For each path, we parse the path out of the file and draw accordingly.
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;
size_t total = 0;
uint32_t* sizes = (uint32_t*)malloc(size * sizeof(uint32_t));
if (sizes == NULL) {
return TVG_E_OUT_OF_MEMORY;
}
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
plutovg_canvas_set_rgb(ctx->canvas, 0, 0, 0);
for (size_t i = 0; i < size; ++i) {
res = tvg_read_varuint(ctx, &sizes[i]);
++sizes[i];
if (res != TVG_SUCCESS) {
goto error;
}
total += sizes[i];
}
res = tvg_apply_style(ctx, style);
if (res != TVG_SUCCESS) {
goto error;
}
for (size_t i = 0; i < size; ++i) {
res = tvg_parse_path(ctx, sizes[i]);
if (res != TVG_SUCCESS) {
goto error;
}
}
plutovg_canvas_fill(ctx->canvas);
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;
size_t total = 0;
uint32_t* sizes = (uint32_t*)malloc(size * sizeof(uint32_t));
if (sizes == NULL) {
return TVG_E_OUT_OF_MEMORY;
}
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
plutovg_canvas_set_rgb(ctx->canvas, 0, 0, 0);
for (size_t i = 0; i < size; ++i) {
res = tvg_read_varuint(ctx, &sizes[i]);
++sizes[i];
if (res != TVG_SUCCESS) {
goto error;
}
total += sizes[i];
}
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) {
goto error;
}
for (size_t i = 0; i < size; ++i) {
res = tvg_parse_path(ctx, sizes[i]);
if (res != TVG_SUCCESS) {
goto error;
}
}
plutovg_canvas_stroke(ctx->canvas);
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;
size_t total = 0;
uint32_t* sizes = (uint32_t*)malloc(size * sizeof(uint32_t));
if (sizes == NULL) {
return TVG_E_OUT_OF_MEMORY;
}
for (size_t i = 0; i < size; ++i) {
res = tvg_read_varuint(ctx, &sizes[i]);
++sizes[i];
if (res != TVG_SUCCESS) {
free(sizes);
return res;
}
total += sizes[i];
}
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
res = tvg_apply_style(ctx, fill_style);
for (size_t i = 0; i < size; ++i) {
res = tvg_parse_path(ctx, sizes[i]);
if (res != TVG_SUCCESS) {
goto error;
}
}
plutovg_canvas_fill_preserve(ctx->canvas);
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) {
goto error;
}
if (line_width == 0) { line_width = .001;
}
plutovg_canvas_set_line_width(ctx->canvas, line_width);
plutovg_canvas_stroke(ctx->canvas);
error:
free(sizes);
return res;
}
The next several functions parse polygons and polylines, which are very similar. The only real difference is a polygon is always a closed path:
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);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
res = tvg_apply_style(ctx, fill_style);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_move_to(ctx->canvas, pt.x, pt.y);
while (--count) {
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) return res;
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
}
tvg_apply_style(ctx, fill_style);
plutovg_canvas_fill(ctx->canvas);
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);
if (res != TVG_SUCCESS) {
return res;
}
plutovg_canvas_move_to(ctx->canvas, pt.x, pt.y);
for (int i = 1; i < size; ++i) {
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
return res;
}
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
}
if (close) {
plutovg_canvas_close_path(ctx->canvas);
}
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) {
return res;
}
if (line_width == 0) { line_width = .001;
}
plutovg_canvas_set_line_width(ctx->canvas, line_width);
plutovg_canvas_stroke(ctx->canvas);
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) {
plutovg_canvas_set_fill_rule(ctx->canvas, PLUTOVG_FILL_RULE_EVEN_ODD);
plutovg_canvas_set_opacity(ctx->canvas, 1.0);
tvg_result_t res = tvg_apply_style(ctx, fill_style);
if (res != TVG_SUCCESS) {
return res;
}
tvg_point_t pt;
res = tvg_read_point(ctx, &pt);
plutovg_canvas_move_to(ctx->canvas, pt.x, pt.y);
for (int i = 1; i < size; ++i) {
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
return res;
}
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
}
if (close) {
plutovg_canvas_close_path(ctx->canvas);
}
plutovg_canvas_fill_preserve(ctx->canvas);
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) {
return res;
}
if (line_width == 0) { line_width = .001;
}
plutovg_canvas_set_line_width(ctx->canvas, line_width);
plutovg_canvas_stroke(ctx->canvas);
return res;
}
Now one final command fulfillment function, this one for a series of lines (not necessarily connected):
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;
for (int i = 0; i < size; ++i) {
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
return res;
}
plutovg_canvas_move_to(ctx->canvas, pt.x, pt.y);
res = tvg_read_point(ctx, &pt);
if (res != TVG_SUCCESS) {
return res;
}
plutovg_canvas_line_to(ctx->canvas, pt.x, pt.y);
}
res = tvg_apply_style(ctx, line_style);
if (res != TVG_SUCCESS) {
return res;
}
if (line_width == 0) { line_width = .001;
}
plutovg_canvas_set_line_width(ctx->canvas, line_width);
plutovg_canvas_stroke(ctx->canvas);
return TVG_SUCCESS;
}
Now finally, the heart of the parsing, parsing the commands. These get read out of the file immediately after the header, and rather than reading until the end of the file, we read until we get a command index of zero which signals the end of the TVG:
It's a long function, but all it's really doing is reading command data, and dispatching the appropriate functions depending on what it finds.
static tvg_result_t tvg_parse_commands(tvg_context_t* ctx) {
tvg_result_t res = TVG_SUCCESS;
uint8_t cmd = 255;
while (cmd != 0) {
if (1 > ctx->inp(&cmd, 1, ctx->inp_state)) {
return TVG_E_IO_ERROR;
}
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);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_fill_polygon(ctx, data.size, &data.style);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_FILL_RECTANGLES: {
tvg_fill_header_t data;
res =
tvg_parse_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_fill_rectangles(ctx, data.size, &data.style);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_FILL_PATH: {
tvg_fill_header_t data;
res =
tvg_parse_fill_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_fill_paths(ctx, data.size, &data.style);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_DRAW_LINES: {
tvg_line_header_t data;
res =
tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_lines(ctx, data.size, &data.style,
data.line_width);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_DRAW_LINE_LOOP: {
tvg_line_header_t data;
res =
tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_polyline(ctx, data.size, &data.style,
data.line_width, true);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_DRAW_LINE_STRIP: {
tvg_line_header_t data;
res =
tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_polyline(ctx, data.size, &data.style,
data.line_width, false);
if (res != TVG_SUCCESS) {
return res;
}
} break;
case TVG_CMD_DRAW_LINE_PATH: {
tvg_line_header_t data;
res =
tvg_parse_line_header(ctx, TVG_CMD_STYLE_KIND(cmd), &data);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_line_paths(ctx, data.size, &data.style,
data.line_width);
if (res != TVG_SUCCESS) {
return res;
}
} 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);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_line_fill_polyline(
ctx, data.size, &data.fill_style, &data.line_style,
data.line_width, true);
if (res != TVG_SUCCESS) {
return res;
}
} 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);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_line_fill_rectangles(
ctx, data.size, &data.fill_style, &data.line_style,
data.line_width);
if (res != TVG_SUCCESS) {
return res;
}
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);
if (res != TVG_SUCCESS) {
return res;
}
res = tvg_parse_line_fill_paths(
ctx, data.size, &data.fill_style, &data.line_style,
data.line_width);
if (res != TVG_SUCCESS) {
return res;
}
} break;
default:
return TVG_E_INVALID_FORMAT;
}
}
return TVG_SUCCESS;
}
The next function is the first external API function in the file. It retrieves the dimensions of the document in pixels, as recorded in the file header:
extern tvg_result_t tvg_document_dimensions(tvg_input_func_t inp, void* inp_state,
uint32_t* out_width,
uint32_t* out_height) {
tvg_context_t ctx;
if (inp == NULL) {
return TVG_E_INVALID_ARG;
}
ctx.inp = inp;
ctx.inp_state = inp_state;
ctx.colors = NULL;
ctx.colors_size = 0;
tvg_result_t res = tvg_parse_header(&ctx, 1);
if (res != TVG_SUCCESS) {
return res;
}
*out_width = ctx.width;
*out_height = ctx.height;
return res;
}
Passing 1 as the second argument to tvg_parse_header()
causes it to early out as soon as the width and height can be computed.
Finally, the primary API function: tvg_render_document()
:
extern tvg_result_t tvg_render_document(tvg_input_func_t inp, void* inp_state,
plutovg_canvas_t* canvas,
const plutovg_rect_t* bounds) {
tvg_context_t ctx;
if (inp == NULL) {
return TVG_E_INVALID_ARG;
}
ctx.inp = inp;
ctx.inp_state = inp_state;
ctx.canvas = canvas;
ctx.colors = NULL;
ctx.colors_size = 0;
tvg_result_t res = tvg_parse_header(&ctx, 0);
if (res != TVG_SUCCESS) {
goto error;
}
float scale_x = bounds->w / ctx.width;
float scale_y = bounds->h / ctx.height;
plutovg_matrix_t m;
plutovg_matrix_init_scale(&m, scale_x, scale_y);
plutovg_matrix_translate(&m, bounds->x / scale_x, bounds->y / scale_y);
plutovg_canvas_set_matrix(ctx.canvas, &m);
res = tvg_parse_commands(&ctx);
if (res != TVG_SUCCESS) {
goto error;
}
error:
if (ctx.colors != NULL) {
free(ctx.colors);
ctx.colors = NULL;
ctx.colors_size = 0;
}
return res;
}
Now for the last mile stuff - the input function (for a FILE
object in this case), and the entry point main()
:
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);
}
int main(int argc, char* argv[]) {
float scale = 1.f;
const char* input = "..\\..\\everything-32.tvg";
const char* output = "..\\..\\output.png";
FILE* inp_file = fopen(input, "rb");
uint32_t w, h;
tvg_result_t res = tvg_document_dimensions(inp_func, inp_file, &w, &h);
if (res != TVG_SUCCESS) {
fprintf(stderr, "Unable to parse '%s'\n", input);
return 1;
}
fseek(inp_file, 0, SEEK_SET);
plutovg_surface_t* surface =
plutovg_surface_create((int)(w * scale), (int)(h * scale));
plutovg_canvas_t* canvas = plutovg_canvas_create(surface);
plutovg_rect_t r = {0.f, 0.f, w * scale, h * scale};
res = tvg_render_document(inp_func, inp_file, canvas, &r);
if (res != TVG_SUCCESS) {
fprintf(stderr, "Unable to parse '%s' - error (%d)\n", input, (int)res);
}
if (!plutovg_surface_write_to_png(surface, output)) {
fprintf(stderr, "Unable to write '%s'\n", output);
goto cleanup;
} else {
fprintf(stderr, "Wrote '%s'\n", output);
}
cleanup:
fclose(inp_file);
plutovg_canvas_destroy(canvas);
plutovg_surface_destroy(surface);
return 0;
}
This is set up to run out of the build directory tree, locating the documents a couple of folders up. It also assumes Windows because of the path backslashes but that's easily changed. It can be pretty readily modified to accept arguments from the command line.
History
- 10th October, 2024 - Initial submission