Specification
Specification
(Working Draft)
Abstract
Features
• Binary encoding
• Support of the most common 2D vector primitives
– Paths
– Polygons
– Rectangles
– Lines
• 3 different fill styles
– Flat color
– Linear 2-point gradient
– Radial 2-point gradient
• Dense encoding, there are near zero padding bits and every byte is used as good as possible.
Recognizing TinyVG
TinyVG is using the .tvg file extension and should use the image/tinyvg mime type.
The textual representation should use the .tvgt file extension and the text/tinyvg mime type.
Display Units
Contrary to pixel graphics, vector graphics don’t have a inherent unit system. While pixels in a bitmap
map 1:1 to pixels on a screen, a vector graphic unit does not have this requirement.
TinyVG uses an abstract unit called display unit which is defined to be a 1/96th of an inch. This matches
the CSS pixel definition so a TinyVG graphic with 48x48 display units will match a typical 48x48 bitmap.
Coordinate system
TinyVG uses the 2-dimensional Cartesian coordinate system with X being the positive horizontal distance
to the origin and Y being the negative vertical distance to the origin. This means that X is going right,
while Y is going down, to match the coordinate system of several other image formats:
1
Binary Encoding
TinyVG files are roughly structured like this:
Header
Color Table
Command
Command
Command
Command
End Of File
Files are made up of a header, followed by a color lookup table and a sequence of commands terminated
by a end of file command.
Concrete color values will only be present in the color table. After the table, only indices into the color
table are used to define color values. This allows to keep the format small, as the first 128 colors in the
vector data are encoded as only a single byte, even if the color format uses 16 bytes per color. This
means in the worst case, we add a single byte to the size of a color that is only used once, but colors
that are common in the file will be encoded as a single byte per use + one time overhead. This encoding
scheme was chosen as a vector graphic typically doesn’t use as many different colors as bitmap graphics
and thus can be encoded more optimally.
Notes
• The following documentation uses a tabular style to document structures.
• All integers are assumed to be encoded in little-endian byte order if not specified otherwise.
• The Type column of each structure definition uses a Zig notation for types and the fields have no
padding bits in between. If a field does not align to a byte boundary, the next field will be offset
into the byte by the current fields bit offset + bit size. This means, that two consecutive fields a
(u3) and b (u5) can be extracted from the byte by using (byte & 0x7) >> 0 for a and (byte &
0xF8) >> 3 for b.
• If not specified otherwise, all coordinates in TinyVG are absolute coordinates, including path nodes
and gradients.
2
• A lot of encoded integers are encoded off-by-one, thus mapping 0 to 1, 1 to 2 and so on. This is
done as encoding these integers as 0 would be equivalent to removing the element from the file.
Thus, this can be used to encode some more elements with less bytes. If this is the case, this is
signaled by the use of value+1.
Header
Each TVG file starts with a header defining some global values for the file like scale and image size. The
header is always at offset 0 in a file.
Color Encoding
The color encoding defines which format the colors in the color table will have:
Coordinate Range
The coordinate range defines how many bits a Unit value uses:
3
VarUInt
This type is used to encode 32 bit unsigned integers while keeping the number of bytes low. It is encoded
as a variable-sized integer that uses 7 bit per byte for integer bits and the 7th bit to encode that there
is ”more bits available”.
The integer is still built as a little-endian, so the first byte will always encode bits 0…6, the second one
encodes 8…13, and so on. Bytes are read until the uppermost bit in the byte is not set. The bit mappings
are done as following:
So a VarUInt always has between 1 and 5 bytes while mapping the full range of a 32 bit value. This
means we only have 5 bit overhead in the worst case, but for all smaller values, we reduce the number
of bytes for encoding unsigned integers.
Encoding Examples The following table contains some examples on how a VarUInt is encoded as a
byte sequence. The byte sequence is written in hexadecimal to allow uniform notation.
Example Code
fn read() u32 {
var count = 0;
var result = 0;
while (true) {
const byte = readByte();
const val = (byte & 0x7F) << (7 * count);
result |= val;
if ((byte & 0x80) == 0)
break;
count += 1;
}
return result;
}
4
while (iter >= 0x80) {
writeByte(0x80 | (iter & 0x7F));
iter >>= 7;
}
writeByte(iter);
}
Color Table
The color table encodes the palette for this file. It’s binary content is defined by the color_encoding field
in the header. For the three defined color encodings, each will yield a list of color_count RGBA tuples.
RGBA 8888
Each color value is encoded as a sequence of four bytes:
RGB 565
Each color value is encoded as a sequence of 2 bytes:
The size of the color table is 2 * color_count, and all colors are fully opaque.
This color encoding uses the sRGB color space.
RGBA F32
Each color value is encoded as a sequence of 16 bytes:
5
Custom
The TinyVG specification does not describe the size nor format of this kind of color table. An implemen-
tation specific format is expected. A conforming parser is allowed to reject files with this color format
as ”unsupported”.
Commands
TinyVG files contain a sequence of draw commands that must be executed in the defined order to get
the final result. Each draw command adds a new 2D primitive to the graphic.
The following commands are available:
End Of Document
If this command is read, the TinyVG file has ended. This command must have prim_style_kind to be
set to 0, so the last byte of every TinyVG file is 0x00.
Every byte after this command is considered not part of the TinyVG data and can be used for other
purposes like metadata or similar.
Fill Polygon
Fills a polygon with N points.
The command is structured like this:
The offset in point_count is there due to 0 points not making any sense at all, and the command could
just be skipped instead of encoding it with 0 points. The offset is 1 to allow code sharing between other
fill commands, as each fill command shares the same header.
point_count must be at least 2, files that encode a lower value must be discarded as ”invalid” by a
6
conforming implementation.
The polygon specified in polygon must be drawn using the even-odd rule, that means that if for any
point to be inside the polygon, a line to infinity must cross an odd number of polygon segments.
Units The unit is the common type for both positions and sizes in the vector graphic. It is encoded
as a signed integer with a configurable amount of bits (see Coordinate Range) and fractional bits.
The file header defines a scale by which each signed integer is divided into the final value. For example,
with a reduced value of 0x13 and a scale of 4, we get the final value of 1.1875, as the number is interpretet
as binary b0001.0011.
Fill Rectangles
Fills a list of rectangles.
The command is structured like this:
The offset in rectangle_count is there due to 0 rectangles not making any sense at all, and the command
could just be skipped instead of encoding it with 0 rectangles. The offset is 1 to allow code sharing
between other fill commands, as each fill command shares the same header.
The rectangles must be drawn first to last, which is the order they appear in the file.
Rectangle
7
Fill Path
Fills a path. Paths are described further below in more detail to keep this section short.
The command is structured like this:
The offset in segment_count is there due to 0 segments don’t make sense at all and the command could
just be skipped instead of encoding it with 0 segments. The offset is 1 to allow code sharing between
other fill commands, as each fill command shares the same header.
For the filling, all path segments are considered a polygon each (drawn with even-odd rule) that, when
overlap, also perform the even odd rule. This allows the user to carve out parts of the path and create
arbitrarily shaped surfaces.
Draw Lines
Draws a set of lines.
The command is structured like this:
Draws line_count + 1 lines with line_style. Each line is line_width units wide, and at least a single
display pixel. This means that line_width of 0 is still visible, even though only marginally. This allows
very thin outlines.
Line
8
Field Type Description
line_width Unit The width of the line.
points [point_count + 1]Point The points of the polygon.
Draws point_count + 1 lines with line_style. Each line is line_width units wide.
The lines are drawn between consecutive points as well as the first and the last point.
9
Field Type Description
segment_count u6 The number of points in the polygon. This value
is offset by 1.
sec_style_kind u2 The secondary style used in this command.
fill_style Style(prim_style_kind) The style that is used to fill the polygon.
line_style Style(sec_style_kind) The style that is used to draw the outline of the
polygon.
line_width Unit The width of the line.
points [segment_count+1]Point The set of points of this polygon.
This command is a combination of Fill Polygon and Draw Line Loop. It first performs a Fill Polygon
with the fill_style, then performs Draw Line Loop with line_style and line_width.
The outline commands use a reduced number of elements, the maximum number of points is 64.
For each rectangle, it is first filled, then its outline is drawn, then the next rectangle is drawn. This
allows to overlap rectangles to look like this:
The outline commands use a reduced number of elements, the maximum number of points is 64.
10
Field Type Description
segment_count u6 The number of points in the polygon. This value is offset
by 1.
sec_style_kind u2 The secondary style used in this command.
fill_style Style(prim_style_kind) The style that is used to fill the polygon.
line_style Style(sec_style_kind) The style that is used to draw the outline of the polygon.
line_width Unit The width of the line.
path Path(segment_count+1) The path that should be drawn.
This command is a combination of Fill Path and Draw Line Path. It first performs a Fill Path with the
fill_style, then performs Draw Line Path with line_style and line_width.
The outline commands use a reduced number of elements, the maximum number of points is 64.
Style(style_type)
There are three types of style available:
Flat Colored
The shape is uniformly colored with the color at color_index in the color table.
Linear Gradient
The gradient is formed by a mental line between point_0 and point_1. The color at point_0 is the
color at color_index_0 in the color table, the color at point_1 is the color at color_index_1 in the color
table.
On the line, the color is interpolated between the two points. Each point that is not on the line is
orthogonally projected to the line and the color at that point is sampled. Points that are not projectable
onto the line have either the color at point_0 if they are closed to point_0 or vice versa for point_1.
11
See the Color Interpolation chapter on how to perform the color interpolation in detail.
Radial Gradient
The gradient is formed by a mental circle with the center at point_0 and point_1 being somewhere on
the circle outline. Thus, the radius of said circle is the distance between point_0 and point_1.
The color at point_0 is the color at color_index_0 in the color table, the color on the circle outline is
the color at color_index_1 in the color table.
If a sampled point is inside the circle, the color is interpolated based on the distance to the center and
the radius. If the point is not in the circle itself, the color at color_index_1 is always taken.
See the Color Interpolation chapter on how to perform the color interpolation in detail.
Path(segment_count)
Paths describe instructions to create complex 2D graphics.
The mental model to form the path is this:
Each path segment generates a shape by moving a ”pen” around. The path this ”pen” takes is the outline
of our segment. Each segment, the ”pen” starts at a defined position and is moved by instructions. Each
instruction will leave the ”pen” at a new position. The line drawn by our ”pen” is the outline of the
shape.
The following instructions to move the ”pen” are available:
12
Field Type Description
instruction u3 The instruction kind as listed in the table above.
padding u1 Always 0
has_line_width u1 If 1, a line width is present.
padding u3 Always 0
Path encoding example As this is a very untypical kind of encoding, the following example will
showcase how a path is encoded. The path will have 3 segments of different length. For conciseness, the
encoding of each individual path component is left out. The unit format is default (16 bit coordinates)
with 2 bit precision.
Line
The line instruction draws a straight line to the position.
Horizontal Line
The horizontal line instruction draws a straight horizontal line to a given x coordinate.
Vertical Line
The vertical line instruction draws a straight vertical line to a given y coordinate.
Cubic Bézier
The cubic bezier instruction draws a Bézier curve with two control points.
13
Field Type Description
control_0 Point The first control point.
control_1 Point The second control point.
point_1 Point The end point of the Bézier curve.
The curve is drawn between the current location and point_1 with control_0 being the first control
point and control_1 being the second one.
Arc Circle
Draws a circle segment between the current and the target point.
radius determines the radius of the circle. If the distance between the current point and target is larger
than radius, the distance is used as the radius.
When large_arc is 1, the larger circle segment is drawn.
If sweep is 1, the circle segment will make a left turn, otherwise it will make a right turn. This means
that if we go from the current point to target, a rotation to the movement direction is necessary to either
the left or the right.
Arc Ellipse
Draws an ellipse segment between the current and the target point.
radius_x and radius_y determine the both radii of the ellipse. If the distance between the current point
and target is not enough to fit any ellipse segment between the two points, radius_x and radius_y are
scaled uniformly so that it fits exactly.
When large_arc is 1, the larger circle segment is drawn.
If sweep is 1, the ellipse segment will make a left turn, otherwise it will make a right turn. This means
that if we go from the current point to target, a rotation to the movement direction is necessary to either
the left or the right.
Close Path
A straight line is drawn to the start location of the current segment. This instruction doesn’t have
additional data encoded.
14
Quadratic Bézier
The quadratic bezier instruction draws a Bézier curve with a single control point.
The curve is drawn between the current location and point_1 with control being the control point.
Rendering
This chapter specifies details of the TinyVG rendering so all images will look the same on different
platforms.
The following formulas and code examples use color intensity values between 0.0 and 1.0.
Alpha Blending
Alpha blending describes the process of blending two transparent colors over each other. As TinyVG has
a transparent background by default, transparency must be respected when blending colors together.
// Blends the s r c c o l o r over the dst c o l o r
RGBA blend (RGBA dst , RGBA s r c ) {
i f ( s r c . a == 0) {
r e t u r n dst ;
}
i f ( s r c . a == 1 . 0 ) {
return src ;
}
return RGBA(
.r = l e r p C o l o r ( s r c . r , dst . r , s r c . a , dst . a , fin_alpha ) ,
.g = l e r p C o l o r ( s r c . g , dst . g , s r c . a , dst . a , fin_alpha ) ,
.b = l e r p C o l o r ( s r c . b , dst . b , s r c . a , dst . a , fin_alpha ) ,
.a = alpha ,
);
}
r e t u r n toColorSpace ( value ) ;
}
15
Color Interpolation
Color interpolation is needed in gradients and must performed in linear color space. This means that
the value from the color table needs to be converted to linear color space, then each color component is
interpolated linearly and the final color is then determined by converting the color back to the specified
color space.
RGBA blend (RGBA f i r s t , RGBA second , f l o a t f ) {
f = clamp ( f , 0 . 0 , 1 . 0 ) ;
r e t u r n RGBA {
. rgb = toColorSpace (
l e r p ( toLinear ( f i r s t . rgb ) , toLinear ( second . rgb ) , f )
),
. a = l e r p ( f i r s t . a , second . a , f ) ,
);
}
lerp (a , b , float f ) {
r e t u r n a + (b - a ) * f ;
}
Line Rendering
Lines are rendered with round line caps and use a total width. Lines in TinyVG can be seen as the
Minkowski sum of a sphere with the line width a diameter and the line itself.
Lines that have a width less than a pixel on the final display, they should be rendered as exactly on pixel
wide. Otherwise lines might get invisible, jagged or otherwise incomplete.
Revision History
1.0
• Initial release
16