IconVG is a compact, binary format for simple vector graphics: icons, logos, glyphs and emoji.
WARNING: THIS FORMAT IS EXPERIMENTAL AND SUBJECT TO INCOMPATIBLE CHANGES.
An IconVG graphic consists of a Magic Identifier, one or more Metadata bytes (8-bit octets) then a sequence of ops (also called operations, instructions or IconVG bytecode) for a register-based VM (Virtual Machine). Ops encode a sequence of filled paths. Conceptually, it repeats three steps:
- Define geometry (combining linear, quadratic and cubic Bézier segments).
- Define paint (flat colors or gradients).
- Fill the geometry with the paint.
The second step can often be skipped, as previously defined paint (saved to VM registers) can be re-used. Repeated geometry (including under affine transformation) can also be factored out, like function calls in other VMs.
There is no explicit representation of path strokes: no stroke width, caps, joins or dashes. IconVG is a presentation format, not an authoring format. Vector graphic authoring tools should flatten strokes and groups into simple filled paths when exporting to IconVG.
IconVG graphics work in 32-bit alpha-premultiplied color, with 8 bits for red,
green, blue and alpha. Using RR:GG:BB:AA
color notation (four colon-separated
two-hexadecimal-digit numbers), alpha-premultiplication means that
00:C0:00:C0
represents a 75%-opaque, 100% Saturation, 100% Value green (with
120° Hue).
An RGBA 4-tuple is sensible (under alpha-premultiplication) if (R <= A)
and
(G <= A)
and (B <= A)
. Depending on further context, a non-sensible value
may mean an invalid IconVG file or it might still be valid (if there is an
alternative interpretation of the 4-tuple).
Interpolation between explicit gradient stops also uses alpha-premultiplied
color, unlike SVG. The color 80:80:80:80
, almost-halfway between opaque
bright red FF:00:00:FF
and transparent black 00:00:00:00
, is a 50% opaque
bright red, not a 50% opaque dark red. The halfway point still has 100%
Saturation and 100% Value (in the HSV Hue Saturation Value sense). It just has
smaller alpha.
VM state includes a Global Alpha value Gα
, a fractional value ranging between
0 and 1 inclusive at 1/255 granularity, initialized to 1.
VM state includes a 3-by-2 Global Forward Transformation Matrix GFTM = [Fa, Fb, Fc; Fd, Fe, Ff]
of floating point values. Implementations may use
float32
, float64
or other mechanisms for floating point computation. IconVG
is not a pixel-exact image format.
For any coordinate pair (x, y)
, the transformed point GFTM(x, y)
is:
GFTMx = (Fa * x) + (Fb * y) + Fc
GFTMy = (Fd * x) + (Fe * y) + Ff
The GFTM implicitly defines a Global Backwards Transformation Matrix GBTM = [Ba, Bb, Bc; Bd, Be, Bf]
, given by:
FDet = (Fa * Fe) - (Fb * Fd)
Ba = +Fe / FDet
Bb = -Fb / FDet
Bc = ((Fb * Ff) - (Fe * Fc)) / FDet
Bd = -Fd / FDet
Be = +Fa / FDet
Bf = ((Fd * Fc) - (Fa * Ff)) / FDet
Any update to the GFTM implicitly updates the GBTM.
The GFTM and GBTM are both initialized to the identity matrix [1, 0, 0; 0, 1, 0]
.
If the determinant FDet
is infinite (in the floating point sense) or if its
absolute value is less than 1e-20
then IconVG implementations may set the
GBTM
to the identity matrix.
A SegRef (Segment Reference) locates a slice of the IconVG file, called the
segment contents. The location consists of a uint64
Segment Offset (the
number of bytes relative to the start of the file) and a uint64
Segment
Length, both measured in bytes. It is invalid for the Segment Offset plus
Segment Length to overflow a uint64
.
A SegRef also includes a uint8
Segment Type describing the contents'
semantics. 0x00
means IconVG bytecode. All other values are reserved.
A SegRef encodes in 8 bytes and there are three forms within that. The first two have restrictions on the Segment Offset and Segment Length:
- An Inline SegRef's low 8 bits hold the Segment Type. The 24 bits after that hold the Segment Length. The high 32 bits are all zero. The Segment Offset is implicit: it immediately follows those 8 bytes.
- An Absolute SegRef (Direct)'s highest bit must be zero. The low 32 bits pack the same as for an Inline SegRef: 8 bits Segment Type then 24 bits Segment Length. The 31 remaining bits hold the Segment Offset and cannot all be zero.
- An Absolute SegRef (Indirect)'s highest bit must be one. The low 8 bits hold the Segment Type. The middle 55 bits are a file offset locating 16 additional bytes that hold a 64-bit Segment Length and then a 64-bit Segment Offset.
Throughout IconVG, integers are unsigned and little-endian unless otherwise noted.
VM state includes 64 general registers REGS[0], REGS[1], ..., REGS[63]
. Each
register is 64 bits wide. Register indexing is done modulo 64, so REGS[70]
is
the same as REGS[6]
, and REGS[-1]
is the same as REGS[63]
.
VM state includes a selector register SEL
. It is effectively a 6 bit integer,
as it indexes REGS
.
A register's low 32 bits can hold a gradient stop position. Its high 32 bits can hold a color. A future IconVG version may assign further semantics to those bits.
Each register's low 32 bits represents an unsigned 16.16 fixed point value for
a gradient stop position. For example, if REGS[7] = 0xFEDC_BA98_0000_4000
then its low 32 bits represent the fraction 0.25
.
Each register's high 32 bits represents an alpha-premultiplied RGBA color,
directly or indirectly. Letting [I ..= J]
and [I .. J]
denote inclusive and
half-exclusive ranges:
- Bits
[32 ..= 39]
hold theR
(red) channel. - Bits
[40 ..= 47]
hold theG
(green) channel. - Bits
[48 ..= 55]
hold theB
(blue) channel. - Bits
[56 ..= 63]
hold theA
(alpha) channel.
If those four values form a sensible alpha-premultiplied color then the
represented color is simply that color. For example, if REGS[40] = 0xC000_C000_7654_3210
then its high 32 bits represent that same 75%-opaque
green, 00:C0:00:C0
, mentioned above.
If those four values do not form a sensible alpha-premultiplied color then the high 32 bits represent a linear blend of two other colors:
- Bits
[32 ..= 39]
hold theBlend
. - Bits
[40 ..= 47]
hold theColRef0
Color Reference. - Bits
[48 ..= 55]
hold theColRef1
Color Reference. - Bits
[56 ..= 63]
is ignored (other than the 4-tuple being non-sensible).
Blend
weights the two Color References. A Blend
value of 64
out of 255
means that the resultant color is roughly three quarters ColRef0
and one
quarter ColRef1
. Specifically, if the Color References ColRef0
and
ColRef1
resolve to RGBA colors Color0
and Color1
then the resultant R
(red) value is given by:
Weight0 = 255 - Blend
Weight1 = Blend
Resultant.R = ((Weight0 * Color0.R) + (Weight1 * Color1.R) + 128) / 255
This is rounded down to an integer value (the mathematical floor function), and likewise for the green, blue and alpha channels.
There are three categories of 1-byte Color References:
- Values
[0x00 ..= 0x7F]
index the 128-entry Built-In Palette. - Values
[0x80 ..= 0xBF]
index the 64-entry Custom Palette. - Values
[0xC0 ..= 0xFF]
are an index-offset to anotherREGS
register.
For the last category, the Color Reference value is added (modulo 64) to the
index of the original blended color to identify another register (or the same
register if the index-offset is 0xC0
), called the target register.
If the target register's high 32 bits holds a sensible alpha-premultiplied
color then resolving the Color Reference produces that color. Otherwise, it
resolves to transparent black 00:00:00:00
. Either way, resolution always
produces a sensible alpha-premultiplied color with no further recursion to
blend other Color References.
For example, if REGS[21] = 0x0081_D340_7654_3210
then its high 32 bits
represent a blended color that is roughly three quarters ((255 - 64) / 255 ≈ 0.749
) of an index-offset color (the Color Reference 0xD3
) and roughly one
quarter of the 2nd element of the Custom Palette (the Color Reference 0x81
).
Starting from REGS[21]
, the index-offset Color Reference 0xD3
points to the
target register REGS[(21 + 0xD3) % 64]
, which is REGS[40]
.
The first three entries are 00:00:00:00
, 80:80:80:80
and C0:C0:C0:C0
. The
remaining 125 = 5 * 5 * 5
entries are opaque (their alpha value is 0xFF
)
whose red, green and blue values are quantized to 0x00
, 0x40
, 0x80
,
0xC0
or 0xFF
. These 128 distinct colors are arranged so that their
RR:GG:BB:AA
values (as a little-endian uint32
) are in increasing order. The
first and last twelve elements of the built-in palette are therefore:
built_in[0x00] = 00:00:00:00
built_in[0x01] = 80:80:80:80
built_in[0x02] = C0:C0:C0:C0
built_in[0x03] = 00:00:00:FF
built_in[0x04] = 40:00:00:FF
built_in[0x05] = 80:00:00:FF
built_in[0x06] = C0:00:00:FF
built_in[0x07] = FF:00:00:FF
built_in[0x08] = 00:40:00:FF
built_in[0x09] = 40:40:00:FF
built_in[0x0A] = 80:40:00:FF
built_in[0x0B] = C0:40:00:FF
- etc
built_in[0x74] = C0:80:FF:FF
built_in[0x75] = FF:80:FF:FF
built_in[0x76] = 00:C0:FF:FF
built_in[0x77] = 40:C0:FF:FF
built_in[0x78] = 80:C0:FF:FF
built_in[0x79] = C0:C0:FF:FF
built_in[0x7A] = FF:C0:FF:FF
built_in[0x7B] = 00:FF:FF:FF
built_in[0x7C] = 40:FF:FF:FF
built_in[0x7D] = 80:FF:FF:FF
built_in[0x7E] = C0:FF:FF:FF
built_in[0x7F] = FF:FF:FF:FF
Recall the REGS
register bit assignments for blended colors:
- Bits
[32 ..= 39]
hold theBlend
. - Bits
[40 ..= 47]
hold theColRef0
Color Reference. - Bits
[48 ..= 55]
hold theColRef1
Color Reference. - Bits
[56 ..= 63]
is ignored (other than the 4-tuple being non-sensible).
Since an 0x00
Color Reference is transparent black, any non-zero Blend
with
all zeroes in the other 24 bits also resolve to transparent black. Painting
with transparent black does nothing, in terms of modifying pixels, but having
multiple transparent black values may be useful for hit testing. For example,
this invisible shape is transparent black 01:00:00:00
, this other shape is
transparent black 02:00:00:00
, etc, and rasterizers can calculate the first
invisible shape (if any) that covers a particular pixel.
An IconVG graphic's rasterization can be varied by a 64 color palette. For example, an emoji graphic may be rasterized with palette color 0 for skin and palette color 1 for hair. Decorative variations, such as different clothing, can be implemented by palette colors possibly set to completely transparent to switch paths off.
IconVG rasterizer software should allow users to pass an optional 64 color palette. If one isn't provided, the Suggested Palette (see below) should be used. Whichever palette ends up being used is called the Custom Palette.
It is invalid for any of the user-given colors to be non-sensible.
A future IconVG version may allow Metadata to associate names like "skin", "hair" or "bow-tie" to the integer indexes of the Custom Palette.
The low 32 bits of each REGS
element is initialized to zero. The high 32 bits
are initialized from the Custom Palette's RR:GG:BB:AA
values in little-endian
order. REGS[0]
copies from the Custom Palette's first entry, REGS[1]
from
the second entry and so on.
SEL
is initialized to 56 so that, at the start, REGS[SEL+0 .. SEL+8]
can be
set to a vector graphic's hard-coded colors while REGS[SEL+8 .. SEL+16]
accesses the Custom Palette's first eight colors.
An IconVG file uses separate byte encodings for floating point, natural (non-negative integer) and coordinate numbers.
Floating point numbers are always encoded in 4 bytes: the little-endian
encoding of a 32-bit IEEE 754 float32
number. NaN
values are invalid IconVG
but +Inf
, -Inf
and -0.0
are valid. For example, 0x04 0x00 0x80 0x3F
encodes the value
1.000000476837158203125
.
Each natural or coordinate number occupies 1, 2 or 4 bytes in the IconVG file, depending on the low two bits of the first byte:
0x00
means a 4-byte encoding.0x01
means a 1-byte encoding.0x02
means a 2-byte encoding.0x03
means a 1-byte encoding.
It is unusual but still valid for a natural or coordinate number to use a longer encoding when an equivalent shorter encoding exists.
For a 1-byte encoding, the high 7 bits form an integer value in the range [0 .. 1<<7]
. For example, 0x29
encodes the natural number 0x14
or, in
decimal, 20
.
For a 2-byte encoding, the high 14 bits form an integer in the range [0 .. 1<<14]
. For example, 0x5A 0x83
encodes the natural number 0x20D6
or, in
decimal, 8406
.
For a 4-byte encoding, the high 30 bits form an integer in the range [0 .. 1<<30]
. For example, 0x04 0x00 0x80 0x3F
encodes the natural number
0xFE0_0001
or, in decimal, 266_338305
.
For encodings shorter than 4 bytes, let N
be the natural number decoding
(converted to float32
) of those bytes.
For a 1-byte encoding, the coordinate number is (N - 64)
, so that a 1-byte
coordinate ranges in [-64 .. +64]
at integer granularity. For example, the
coordinate number 7
can be encoded as 0x8F
.
For a 2-byte encoding, the coordinate number is ((N - 8192) / 64)
, so that a
2-byte coordinate ranges in [-128 .. +128]
at 1/64
granularity. For
example, the coordinate number 7.5
can be encoded as 0x82 0x87
.
For a 4-byte encoding, the coordinate number just equals the floating point
decoding. Again, NaN
values are invalid IconVG. For example, the coordinate
7.5
can also be encoded as 0x00 0x00 0xF0 0x40
.
An IconVG graphic starts with the four bytes 0x8A 0x49 0x56 0x47
, which is
"\x8aIVG"
.
The Metadata encoding starts with a natural number (the number of metadata
chunks in the Metadata) followed by that many chunks. Each chunk starts with a
natural number ChunkLength
: the number of bytes remaining in the chunk,
excluding the chunk length itself. After that is a MID (Metadata Identifier)
natural number and then the MSD (MID-Specific Data), a variable number of
bytes. It is invalid for the combined MID and MSD to be shorter or longer than
ChunkLength
.
Chunks must be presented in increasing MID order and as MIDs are natural numbers, the minimum MID is zero. MIDs cannot be repeated. All MIDs are optional.
MID 8 means that the MSD contains four coordinate numbers. These are the
MinX
, MinY
, MaxX
and MaxY
of the graphic's ViewBox: its clipping
rectangle in (scalable) vector space. Individual path nodes may be outside of
the ViewBox but, when filling paths, only pixels within the clipping rectangle
are affected.
Coordinates are in abstract units and one unit does not necessarily mean one pixel. The ViewBox's aspect ratio is also a hint (but not a mandate) for the rasterized pixels' aspect ratio. Implementations may rasterize a "naturally 3:2" IconVG graphic as 300x200 or as 45x30 but may also (non-uniformly) scale it to create a 100x100 pixel image.
A zero-width or zero-height ViewBox (i.e. a degenerate clipping rectangle) is valid, resulting in an empty but valid graphic in the same way that an empty character string is valid.
If this MID is not present, the ViewBox defaults to (-32, -32, +32, +32)
. A
ViewBox is invalid if (MinX > MaxX)
, if (MinY > MaxY)
or if any of those
four coordinate numbers are infinite (or NaN
).
MID 16 means that the MSD contains a Suggested Palette. This provides default values for variable colors, e.g. for an emoji's skin and hair.
The MSD starts with a 1-byte 'PalCount', invalid if (PalCount > 63)
. There
are (4 * (PalCount + 1))
bytes after that, being (PalCount + 1)
RGBA colors
in {R0, G0, B0, A0, R1, G1, etc}
byte order. It is invalid for any of these
colors to be non-sensible.
The remaining (63 - PalCount)
entries of the suggested palette are implicitly
set to opaque black 00:00:00:FF
.
If this MID is not present, the suggested palette consists entirely of opaque black, as black is always fashionable.
Ops occupy a variable but integer number of bytes and the first byte of each op is called its opcode. There are four opcode categories, identified by their high two bits:
- Opcodes
[0x00 ..= 0x3F]
are Geometry, Miscellaneous and Control Flow ops. - Opcodes
[0x40 ..= 0x7F]
are Register ops. - Opcodes
[0x80 ..= 0xCF]
are Fill ops. - Opcodes
[0xD0 ..= 0xFF]
are Reserved ops.
Some op descriptions refer to a LOW4
value. This is the low four bits of the
opcode, in the range [0 ..= 15]
.
The VM state includes a Current Path, open at the Pen Position (also called
(PPx, PPy)
). The Current Path is initially empty but starts at the origin
(0, 0)
and the Pen Position is likewise initialized to (0, 0)
. Geometry ops
add linear, quadratic or cubic Bézier segments to the Current Path:
- Opcodes
[0x00 ..= 0x0F]
are LineTo ops. - Opcodes
[0x10 ..= 0x1F]
are QuadTo ops. - Opcodes
[0x20 ..= 0x2F]
are CubeTo ops.
If LOW4
is non-zero, let RepCount
equal LOW4
. Otherwise, the opcode byte
is followed by a natural number and let RepCount
equal that natural number
plus 16. Either way, after that comes (2 * RepCount)
for LineTo, (4 * RepCount)
for QuadTo or (6 * RepCount)
for CubeTo coordinate numbers.
For example, an 0x19
opcode byte would be followed by 9 groups of 4
coordinate numbers {x1, y1, x2, y2}
. If the groups were labeled {a, b, ..., i}
then the coordinate numbers appear in the sequence {ax1, ay1, ax2, ay2, bx1, by1, bx2, by2, ..., ix1, iy1, ix2, iy2}
. Each group adds a quadratic
Bézier segment to the Current Path, from (PPx, PPy)
to GFTM(x2, y2)
via
GFTM(x1, y1)
. Each coordinate number pair is transformed by the GFTM (Global
Forward Transformation Matrix). Processing a group ends with setting the Pen
Position to the final (transformed) coordinate number pair, GFTM(x2, y2)
.
For example, an 0x21
opcode byte would be followed by 1 group of 6 coordinate
numbers {x1, y1, x2, y2, x3, y3}
, adding a cubic Bézier segment from (PPx, PPy)
to GFTM(x3, y3)
via GFTM(x1, y1)
and GFTM(x2, y2)
and then setting
the Pen Position to GFTM(x3, y3)
.
For example, an 0x00 0x15
byte sequence would be followed by 26 groups
(0x15
encodes the natural number 10 and 10 + 16 = 26) of 2 coordinate numbers
{x1, y1}
, each group adding a linear segment from (PPx, PPy)
to GFTM(x1, y1)
and then setting the Pen Position to GFTM(x1, y1)
.
- Opcodes
[0x30 ..= 0x34]
are the Ellipse and Parallelogram ops.
Each of these ops are followed by exactly 4 coordinate numbers {x1, y1, x2, y2}
. Let A
, B
and C
denote the three points (PPx, PPy)
, GFTM(x1, y1)
and GFTM(x2, y2)
. This implies a fourth point of a parallelogram,
D = A - B + C
.
Starting from A
, the 0x34
Parallelogram op is equivalent to a 4-segment
LineTo
op: to B
, to C
, to D
and finally to A
. The Pen Position does
not change. It stays at A
.
For the [0x30 ..= 0x33]
Ellipse opcodes, define a center point
X = (A + C) / 2
and two axis vectors r = B - X
and s = C - X
. These
aren't necessarily the major (longest) and minor (shortest) axes of the
ellipse. They're just two axes, derived only from A
, B
and C
. We then
derive four cubic Bézier segments:
- The 1st segment's control points are
A
,A+
,B-
andB
. - The 2nd segment's control points are
B
,B+
,C-
andC
. - The 3rd segment's control points are
C
,C+
,D-
andD
. - The 4th segment's control points are
D
,D+
,A-
andA
.
The 8 off-curve control points are defined by a scalar constant k = 0.551784777779014
:
A- = A - k.(B-X) = A - k.r
A+ = A + k.(B-X) = A + k.r
B- = B - k.(C-X) = B - k.s
B+ = B + k.(C-X) = B + k.s
C- = C - k.(D-X) = C + k.r
C+ = C + k.(D-X) = C - k.r
D- = D - k.(A-X) = D + k.s
D+ = D + k.(A-X) = D - k.s
These implicit control points (and the magic number k
) are discussed in
"Three Points (Two Opposing) Define an
Ellipse".
- The
0x30
Quarter Ellipse op adds only the 1st segment to the Current Path, moving the Pen Position toB
. - The
0x31
Half Ellipse op adds the 1st and 2nd segments, moving toC
. - The
0x32
Three Quarter Ellipse op adds the 1st, 2nd and 3rd segments, moving toD
. - The
0x33
Full Ellipse op adds all four segments and leaves the Pen Position unchanged atA
.
- Opcode
0x35
is the ClosePathMoveTo op.
The ClosePathMoveTo op combines two actions (ClosePath, MoveTo) and is followed
by 2 coordinate numbers {x1, y1}
. ClosePath closes the Current Path (adding
an implicit linear segment if the Pen Position isn't at the path start),
appending that closed path to a list of Pending Paths. MoveTo then opens a new
Current Path starting at GFTM(x1, y1)
. The Pen Position also moves to that
point.
- Opcode
0x36
is a 2-byte op that adds (modulo 64) the second byte toSEL
. For example, the byte sequence0x36 0x02
adds 2 toSEL
. - Opcode
0x37
is a 1-byte NOP (no operation; do nothing).
VM state includes a Program Counter (PC), the file offset of the next op to execute. It is initialized to the first byte after all of the Metadata.
The PC usually advances sequentially, being incremented by N after executing an op that occupies N bytes. However, Jump, Call and Return ops can change the PC in other ways.
VM state includes an End Of Bytecode (EOB) uint64
value, initialized to
EOBMax (also called UINT64_MAX = 0xFFFF_FFFF_FFFF_FFFF
).
The VM executes an implicit Return op (see below) when the PC is at the EOB or at the end of the IconVG file. It is invalid for an op's bytes to cross the EOB or to be incomplete because it reached the end of the IconVG file.
Each Jump opcode is followed by a natural number, JumpCount
. These ops all
propel the PC forward (never backward), jumping over the next JumpCount
ops.
That count excludes the Jump op itself, so that a zero JumpCount
Jump is
effectively a NOP (and not an infinite loop). Ops are atomic and Jumps cannot
move the PC into the middle of a multi-byte op.
It is valid to jump to the EOB or the end of the IconVG file exactly (the next op will be an implicit Return), but invalid to jump past either.
- Opcode
0x38
is an Unconditional Jump. The jump is always taken. - Opcode
0x39
is a Feature Detection Jump (FDJump). TheJumpCount
is followed by a natural numberFeaturesNeeded
. The jump is taken unless the rasterizer provides all of those features. See "Feature Detection" below. - Opcode
0x3A
is a Level Of Detail Jump (LODJump). TheJumpCount
is followed by two coordinate numbers,LOD0
andLOD1
. The jump is taken unless the rasterization's height in pixelsH
satisfies both(LOD0 <= H)
and(H < LOD1)
.
The LODJump op allows an IconVG file to provide a simpler version for small rasterizations (e.g. below 32 pixels) and a more complex version for large rasterizations (e.g. 32 and above pixels).
Future IconVG versions may add additional features and provide semantics to previously reserved opcodes. The FDJump mechanism allows newer IconVG files to instruct older or limited IconVG rasterizers to skip over the newer ops that they do not implement and possibly jump to a fallback op sequence instead.
Rasterizers provide an ambient, read-only FeaturesImplemented
uint32
value,
which may be zero. IconVG reserves bits of that overall value for different
features and an FDJump op takes the jump unless the bitwise-and of the op's
FeaturesNeeded
and the rasterizer's FeaturesImplemented
values equals
FeaturesNeeded
. For example, an FDJump needing 0x0000_0103
meeting a
rasterizer implementing 0x0000_00F7
would take the jump as the 0x0000_0100
feature bit was unsatisfied.
All feature bits are reserved. The 0x0000_0001
feature bit is expected to
relate to Animation as a broad concept, although this document does not yet
give further details on how IconVG would specifically represent Animation.
VM state includes a Global Return Address (GRA), a file offset, initialized to zero. IconVG bytecode calls cannot nest. The maximum call stack depth is one.
- Opcode
0x3B
is a Return op.
If the GRA is zero, the Return op ends the graphic (even if we are not at the EOB or the end of the IconVG file). Otherwise, it resets the PC, EOB, Gα and GFTM to the GRA, EOBMax, 1 and the identity matrix (and the GBTM to the identity matrix) and the GRA is then reset to zero.
- Opcode
0x3C
is a Call Untransformed op. - Opcode
0x3D
is a Call Transformed op. - Opcodes
[0x3E ..= 0x3F]
are reserved (see "Reserved Ops" below).
If the GRA is non-zero then the Call op is invalid. Otherwise, it sets the GRA to the file offset after the last byte of the Call op.
The 0x3D
opcode is followed by an αFTM (Alpha and Forward Transformation
Matrix). An αFTM is a 1-byte alpha value (scaled by 255 so that α ranges
between 0 and 1) and then six coordinate numbers forming a 3-by-2 affine
transformation matrix [a, b, c; d, e, f]
. The Gα and GFTM are set to these
values (which also updates the GBTM).
For the 0x3C
opcode, the Gα and GFTM (and GBTM) remain unchanged at 1 and the
identity matrix (and the identity matrix).
The αFTM (or lack of it) is followed by a 8-byte SegRef (e.g. "switch to the
Segment Type 0x2A
instruction set" or "re-use some shared geometry and
paint"). If the SegRef is Inline, the segment contents (the Segment Length
bytes following the SegRef) are considered part of this Call op, when setting
the GRA past "the last byte of the Call op" and when Jump ops jump over the op.
An Absolute SegRef's 8 bytes are also considered part of this Call op, for
those same purposes, but its segment contents are not.
Whether Inline or Absolute, the PC and EOB are set to the segment contents'
start and end offsets and, for IconVG bytecode (Segment Type 0x00
), VM
bytecode execution continues. That start and end may overlap with the top level
bytecode and the start may be in the middle of what was previously considered a
multi-byte op. Op decoding starts afresh when executing the callee bytecode.
Segment Types other than 0x00
are reserved and are invalid to Call in this
version (there is no fallback behavior). Future-versioned IconVG files that use
them should guard them with FDJump ops.
- Opcodes
[0x40 ..= 0x4F]
set the low 32 bits ofREGS[SEL + LOW4]
. The high 32 bits are set to zero. The opcode is followed by 4 bytes. - Opcodes
[0x50 ..= 0x5F]
set the high 32 bits ofREGS[SEL + LOW4]
. The low 32 bits are set to zero. The opcode is followed by 4 bytes. - Opcodes
[0x60 ..= 0x6F]
set all 64 bits ofREGS[SEL + LOW4]
. The opcode is followed by 8 bytes (4 low bytes and then 4 high bytes).
For the three Register Op categories above, if LOW4 == 0
then the op also
decrements SEL
by one, after updating REGS
.
- Opcodes
[0x70 ..= 0x7F]
first decrementsSEL
by(LOW4 + 2)
. The opcode is followed by(8 * (LOW4 + 2))
bytes, which are then copied 8 bytes at a time toREGS[SEL + 1], REGS[SEL + 2], ..., REGS[SEL + LOW4 + 2]
.
When setting the low 32 bits, high 32 bits or all 64 bits of a REGS
register,
the 4 or 8 bytes are copied from the IconVG file in little-endian order.
If LOW4 == 0
then these Fill ops first increment SEL
by one, before doing
anything else. Regardless of LOW4
, they then ClosePath (the first half of the
ClosePathMoveTo op) without updating the Pen Position and then fill any Pending
Paths. Fills use the Winding fill rule ("inside" means a non-zero sum of signed
edge crossings), not the Even-Odd fill rule. The Pending Paths list is then
cleared. These paths are consumed (not preserved) and take no further part in
any future ops.
Some Fill ops provide a Nominal Gradient Matrix NGM = [Na, Nb, Nc; Nd, Ne, Nf]
, discussed below.
- Opcodes
[0x80 ..= 0x8F]
fill with a flat (uniform) color, resolvingREGS[SEL + LOW4]
. - Opcodes
[0x90 ..= 0x9F]
fill with a linear gradient. It is followed by a Gradient Configuration byte and then three floating point numbersNa
,Nb
andNc
. TheNd
,Ne
andNf
numbers are all set to zero. - Opcodes
[0xA0 ..= 0xAF]
fill with a radial gradient. It is followed by a Gradient Configuration byte and then six floating point numbersNa
,Nb
,Nc
,Nd
,Ne
andNf
.
The Gradient Configuration's low 6 bits gives a number in the range [0 ..= 62]
, with 63 being invalid. Adding 2 gives NSTOPS
, the number of gradient
stops, whose position and resolved color come from the low and high 32 bits of
REGS[SEL + LOW4 + 0], REGS[SEL + LOW4 + 1], ..., REGS[SEL + LOW4 + NSTOPS - 1]
. The stop positions (as 16.16 fixed point values) must start at
0, end at 1 and be in non-decreasing order.
The Gradient Configuration's high 2 bits give the Spread (how to extrapolate
gradient colors outside of the [0 ..= 1]
stop position nominal range):
0x00
None means that stop positions below0
and above1
map to transparent black.0x01
Pad means that stop positions below0
and above1
map to the colors that0
and1
would map to.0x02
Reflect means that the stop position mapping is reflected start-to-end, end-to-start, start-to-end, etc.0x03
Repeat means that the stop position mapping is repeated start-to-end, start-to-end, start-to-end, etc.
The Nominal Gradient Matrix NGM = [Na, Nb, Nc; Nd, Ne, Nf]
is multiplied by
the Global Backwards Transformation Matrix GBTM = [Ba, Bb, Bc; Bd, Be, Bf]
to
produce the Effective Gradient Matrix EGM = [Ea, Eb, Ec; Ed, Ee, Ef]
:
Ea = (Na * Ba) + (Nb * Bd)
Eb = (Na * Bb) + (Nb * Be)
Ec = (Na * Bc) + (Nb * Bf) + Nc
Ed = (Nd * Ba) + (Ne * Bd)
Ee = (Nd * Bb) + (Ne * Be)
Ef = (Nd * Bc) + (Ne * Bf) + Nf
This matrix maps from graphic coordinate space (the space where the Metadata's
ViewBox lives) to gradient coordinate space. Gradient coordinate space is where
a linear gradient ranges from x=0
to x=1
, and a radial gradient has center
(0, 0)
and radius 1
.
The graphic coordinate (Px, Py)
maps to the gradient coordinate (Dx, Dy)
by:
Dx = (Ea * Px) + (Eb * Py) + Ec
Dy = (Ed * Px) + (Ee * Py) + Ef
The Appendix below gives
explicit formulae for the [Na, Nb, Nc; Nd, Ne, Nf]
affine transformation
matrix for common gradient geometry, such as a linear gradient defined by two
points.
For example, here is a 14 byte sequence for a linear gradient Fill op and its
[Na, Nb, Nc; 0, 0, 0]
Nominal Gradient Matrix. There are five gradient stops,
from REGS[SEL+1]
inclusive to REGS[SEL+6]
exclusive (or equivalently, to
REGS[SEL+5]
inclusive).
91 43 #0008 ClosePath; Fill (linear gradient; pad) with REGS[SEL+1 .. SEL+6]
88 88 08 3d +0.03333333
88 88 88 3c +0.016666666
24 22 22 3f +0.63333344
Many vector graphic implementations can take a [Na, Nb, Nc; Nd, Ne, Nf]
transformation matrix (or its inverse) directly, but for those that cannot (and
instead take two points (Px1, Py1)
and (Px2, Py2)
to define a linear
gradient), the formulae from the "Inverse Linear
Gradients" section below recovers two such points
as (-19, 0)
and (5, 12)
. The delta between them is (+24, +12)
in the x:y
ratio of +2:1. The vector (+7, -14)
, with ratio -1:2, is orthogonal to that
delta, so an equivalent pair of points would be (-12, -14)
and (+12, -2)
.
This example is the second gradient ("#0008" identifies the 9th op) in the gradient.iconvg.disassembly example file. The test/data directory holds other gradient examples.
Future IconVG versions may redefine these ops' behavior but not how many bytes they occupy. This version defines fallback behavior, so it is not necessary to guard them with FDJump ops.
- Opcodes
[0x3E ..= 0x3F]
are followed by Extra Data. The fallback behavior is a NOP. - Opcodes
[0xB0 ..= 0xBF]
are followed by Extra Data. The fallback behavior is to fill with a flat color as if the opcode was in[0x80 ..= 0x8F]
. This includes pre-incrementingSEL
whenLOW4
is zero. - Opcodes
[0xC0 ..= 0xDF]
are followed by Extra Data and then a coordinate pair(x1, y1)
. The fallback behavior is a single LineTo that point (transformed by GFTM). If a future IconVG version redefines these ops, the new semantics are expected to also move the Pen Position to this final point. - Opcodes
[0xE0 ..= 0xFF]
are followed by Extra Data. The fallback behavior is a NOP.
Extra Data consists of a natural number EDLength
and then EDLength
bytes.
Implementations should skip over them.
An ASCII art rasterization of Material Design's "action/info" icon:
........................
........................
........++8888++........
......+8888888888+......
.....+888888888888+.....
....+88888888888888+....
...+8888888888888888+...
...88888888..88888888...
..+88888888..88888888+..
..+888888888888888888+..
..88888888888888888888..
..888888888..888888888..
..888888888..888888888..
..888888888..888888888..
..+88888888..88888888+..
..+88888888..88888888+..
...88888888..88888888...
...+8888888888888888+...
....+88888888888888+....
.....+888888888888+.....
......+8888888888+......
........++8888++........
........................
........................
The SVG
form
is 202 bytes, or 174 bytes after "gzip --best"
:
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4z
m2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>
The PNG forms at various sizes:
18 x 18: 207 bytes
24 x 24: 222 bytes
36 x 36: 321 bytes
48 x 48: 412 bytes
The IconVG form is 36 bytes:
8a 49 56 47 03 0b 11 51 51 b1 b1 35 81 59 33 59
81 81 a9 35 85 95 34 7d 95 7d 7d 35 85 75 34 7d
75 7d 6d 88
The annotated disassembly is below. Note that the IconVG ViewBox ranges from
-24
to +24
while the SVG viewBox ranges from 0
to 48
.
8a 49 56 47 IconVG Magic Identifier
03 Number of metadata chunks: 1
0b Metadata chunk length: 5
11 Metadata Identifier: 8 (viewBox)
51 -24
51 -24
b1 +24
b1 +24
35 #0000 ClosePath; MoveTo
81 +0
59 -20
33 #0001 Ellipse (4 quarters)
59 -20
81 +0
81 +0
a9 +20
35 #0002 ClosePath; MoveTo
85 +2
95 +10
34 #0003 Parallelogram
7d -2
95 +10
7d -2
7d -2
35 #0004 ClosePath; MoveTo
85 +2
75 -6
34 #0005 Parallelogram
7d -2
75 -6
7d -2
6d -10
88 #0006 ClosePath; Fill (flat color) with REGS[SEL+8]
The test/data directory holds these files and other examples.
This appendix derives the Nominal Gradient Matrices [Na, Nb, Nc; Nd, Ne, Nf]
for linear, circular and elliptical gradients.
For a linear gradient from (Px1, Py1)
to (Px2, Py2)
, let Δx = (Px2 - Px1)
and Δy = (Py2 - Py1)
. In gradient coordinate space, the y
-coordinate is
ignored and so Nd
, Ne
and Nf
can be arbitrary. The transformation matrix
simplifies to [Na, Nb, Nc; 0, 0, 0]
. It satisfies the three simultaneous
equations:
Na*(Px1 ) + Nb*(Py1 ) + Nc = 0 (eq L.0)
Na*(Px1+Δy) + Nb*(Py1-Δx) + Nc = 0 (eq L.1)
Na*(Px1+Δx) + Nb*(Py1+Δy) + Nc = 1 (eq L.2)
Subtracting equation L.0
from equations L.1
and L.2
yields:
Na*Δy - Nb*Δx = 0
Na*Δx + Nb*Δy = 1
So that
Na*Δy*Δy - Nb*Δx*Δy = 0
Na*Δx*Δx + Nb*Δx*Δy = Δx
Overall:
Na = Δx / (Δx*Δx + Δy*Δy)
Nb = Δy / (Δx*Δx + Δy*Δy)
Nc = -a*Px1 - b*Py1
Nd = 0
Ne = 0
Nf = 0
To invert the previous section's calculation, recovering (Px1, Py1)
and
(Px2, Py2)
from [Na, Nb, Nc; 0, 0, 0]
, note that in the non-degenerate
case, there are infinitely many points (Px1, Py1)
and (Px2, Py2)
. This
section derives two of those.
Recast (Px2, Py2)
as (Px1+Δx, Py1+Δy)
. These two points map to Dx=0
and
Dx=1
in gradient space. Furthermore, rotating that delta by 90 degrees gives
another point (Px1+Δy, Py1-Δx)
that also maps to Dx=0
. Solve the
simultaneous equations:
Dx1 = 0 = (Na * (Px1 )) + (Nb * (Py1 )) + Nc
Dx2 = 1 = (Na * (Px1+Δx)) + (Nb * (Py1+Δy)) + Nc
Dx3 = 0 = (Na * (Px1+Δy)) + (Nb * (Py1-Δx)) + Nc
To pick one out of the infinite options, set Py1=0
to simplify:
Dx1 = 0 = (Na * (Px1 )) + Nc
Dx2 = 1 = (Na * (Px1+Δx)) + (Nb * +Δy) + Nc
Dx3 = 0 = (Na * (Px1+Δy)) + (Nb * -Δx) + Nc
The first line gives Px1 = -Nc/Na
. Substituting that into the remaining equations:
1 = (Na * +Δx)) + (Nb * +Δy)
0 = (Na * +Δy)) + (Nb * -Δx)
Adding Nb
times the first to Na
times the second:
Nb = (Na*Na + Nb*Nb) * Δy
So Δy = Nb / (Na*Na + Nb*Nb)
. A similar reduction gives Δx = Na / (Na*Na + Nb*Nb)
. We now know Px1
, Py1
, Δx
and Δy
. To answer the original
question, going from [Na, Nb, Nc; 0, 0, 0]
to a (Px1, Py1)
and (Px2, Py2)
pair that describes the linear gradient:
Px1 = -Nc/Na
Py1 = 0
Px2 = Px1 + Δx = -Nc/Na + (Na / (Na*Na + Nb*Nb))
Py2 = Py1 + Δy = 0 + (Nb / (Na*Na + Nb*Nb))
If Na=0
then replace "set Py1=0
to simplify" with "set Px1=0
to simplify"
and the same process yields similar formulae, picking a different one of the
infinitely many solutions. If both Na=0
and Nb=0
then it's a degenerate
gradient where every point in graphic coordinate space maps to a gradient
offset of Nc
.
For a circular gradient with center (Pcx, Pcy)
and radius vector (Prx, Pry)
, such that (Pcx+Prx, Pcy+Pry)
is on the circle, let
r = math.Sqrt(Prx*Prx + Pry*Pry)
The transformation matrix maps (Pcx, Pcy)
to (0, 0)
, maps (Pcx+r, Pcy)
to
(1, 0)
and maps (Pcx, Pcy+r)
to (0, 1)
. Solving those six simultaneous
equations give:
Na = +1 / r
Nb = +0 / r
Nc = -Pcx / r
Nd = +0 / r
Ne = +1 / r
Nf = -Pcy / r
For an elliptical gradient with center (Pcx, Pcy)
and axis vectors (Prx, Pry)
and (Psx, Psy)
, such that (Pcx+Prx, Pcx+Pry)
and (Pcx+Psx, Pcx+Psy)
are on the ellipse, the transformation matrix satisfies the six simultaneous
equations:
Na*(Pcx ) + Nb*(Pcy ) + Nc = 0 (eq E.0)
Na*(Pcx+Prx) + Nb*(Pcy+Pry) + Nc = 1 (eq E.1)
Na*(Pcx+Psx) + Nb*(Pcy+Psy) + Nc = 0 (eq E.2)
Nd*(Pcx ) + Ne*(Pcy ) + Nf = 0 (eq E.3)
Nd*(Pcx+Prx) + Ne*(Pcy+Pry) + Nf = 0 (eq E.4)
Nd*(Pcx+Psx) + Ne*(Pcy+Psy) + Nf = 1 (eq E.5)
Subtracting equation E.0
from equations E.1
and E.2
yields:
Na*Prx + Nb*Pry = 1
Na*Psx + Nb*Psy = 0
Solving these two simultaneous equations yields:
Na = +Psy / (Prx*Psy - Psx*Pry)
Nb = -Psx / (Prx*Psy - Psx*Pry)
Re-arranging E.0
yields:
Nc = -Na*Pcx - Nb*Pcy
Similarly for Nd
, Ne
and Nf
so that, overall:
Na = +Psy / (Prx*Psy - Psx*Pry)
Nb = -Psx / (Prx*Psy - Psx*Pry)
Nc = -Na*Pcx - Nb*Pcy
Nd = -Pry / (Prx*Psy - Psx*Pry)
Ne = +Prx / (Prx*Psy - Psx*Pry)
Nf = -Nd*Pcx - Ne*Pcy
Note that if Prx = r
, Pry = 0
, Psx = 0
and Psy = r
then this simplifies
to the Circular Gradients formulae above.
Updated on December 2021.