From ccf95637ad9fe8fbb9e949f6f3e9c1beedbb1c0c Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Wed, 29 Dec 2021 11:34:09 +1100 Subject: [PATCH] Upgrade spec to FFV1 (File Format Version 1) Updates #4 Fixes #9 Fixes #11 Fixes #13 Fixes #14 Fixes #15 Fixes #16 Fixes #17 Fixes #18 Fixes #19 Fixes #20 Fixes #21 Fixes #29 --- spec/iconvg-spec.md | 1352 ++++++++++++++++++++++++++----------------- 1 file changed, 835 insertions(+), 517 deletions(-) diff --git a/spec/iconvg-spec.md b/spec/iconvg-spec.md index 670abf0..2d66972 100644 --- a/spec/iconvg-spec.md +++ b/spec/iconvg-spec.md @@ -5,478 +5,775 @@ glyphs and emoji. **WARNING: THIS FORMAT IS EXPERIMENTAL AND SUBJECT TO INCOMPATIBLE CHANGES.** -Specifically, we are considering [**substantial changes to the file -format**](https://github.com/google/iconvg/issues/4) to happen in mid-2021. +## Overview -## Structure +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: -An IconVG graphic consists of a magic identifier, one or more metadata bytes -then a sequence of variable length instructions for a register-based virtual -machine. +1. Define geometry (combining linear, quadratic and cubic Bézier segments). +2. Define paint (flat colors or gradients). +3. Fill the geometry with the paint. -Those instructions encode a sequence of filled paths, similar to [SVG path -data](https://www.w3.org/TR/SVG/paths.html#PathData). Rendering involves -switching between two modes: a *styling mode* where color and other registers -are set and a *drawing mode* where a path's geometry is defined. The virtual -machine starts in the styling mode. +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. -In both modes, rendering proceeds by reading a one byte opcode followed by a -variable number of data bytes for that opcode. The mapping from byte values to -opcodes depends on whether the renderer is in the styling or drawing mode. A -`0x07` byte value means setting the color selector register in the styling -mode and means adding multiple lineto segments in the drawing mode. +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. -## Registers +### Color -The machine state includes 64 *color registers* (denoted `CREG[0], CREG[1], -..., CREG[63]`) and 64 *number registers* (denoted `NREG[0], NREG[1], ..., -NREG[63]`). Register indexing is done modulo 64, so `CREG[70]` is the same as -`CREG[6]`, and `CREG[-1]` is the same as `CREG[63]`. +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). -Each `CREG` and `NREG` register is 32 bits wide. The `CREG` registers are -initialized to the custom palette ([see below](#palettes)); the `NREG` -registers are initialized to `0`. The machine state also includes two *selector -registers* (denoted `CSEL` and `NSEL`). They are effectively 6 bit integers, as -they index `CREG` and `NREG`, and are also initialized to `0`. +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). -Color registers are four `uint8` values (red, green, blue and alpha), denoted -by colon-separated 2-hexadecimal-digit numbers in `RR:GG:BB:AA` order. +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. -Number registers are `float32` values. +### Global Alpha and Transformation Matrices -### Level of Detail +VM state includes a Global Alpha value `Gα`, a fractional value ranging between +0 and 1 inclusive at 1/255 granularity, initialized to 1. -The machine state includes 2 *level of detail registers* (denoted `LOD0` and -`LOD1`, both `float32` values) initialized to `+0` and `+infinity`. Drawing -mode opcodes have no effect (other than leaving drawing mode) unless the height -`H` in pixels of the rasterization satisfies `(LOD0 <= H)` and `(H < LOD1)`. +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. -This allows the format 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). +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 -## Colors and Gradients +The GFTM implicitly defines a Global Backwards Transformation Matrix `GBTM = +[Ba, Bb, Bc; Bd, Be, Bf]`, given by: -IconVG graphics work in 32 bit alpha-premultiplied color, with 8 bits for red, -green, blue and alpha. Alpha-premultiplication means that `00:C0:00:C0` -represents a 75%-opaque, 100% Saturation, 100% Value green (with 120° Hue). + 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 -Interpolation between explicit gradient stops also uses alpha-premultiplied -color, unlike SVG. The halfway color between opaque bright red = `RGBA(1, 0, 0, -1)` and transparent black = `RGBA(0, 0, 0, 0)`, which is `RGBA(½, 0, 0, ½)`, 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. - -Alpha-premultiplication also means that some `RGBA` combinations (where e.g. -the red value is greater than the alpha value) are nonsensical. The virtual -machine re-purposes some of those values to represent *gradients* instead of -flat colors. Any color register whose alpha value is `0` but whose blue value -is at least `128` is a gradient. Its remaining bits are reinterpreted such -that: - -- The low 6 bits of the red value is the number of color/offset stops, - `NSTOPS`. -- The high 2 bits of the red value are reserved. -- The low 6 bits of the green value is the color register base, `CBASE`. -- The high 2 bits of the green value is how to spread the gradient past its - nominal bounds (from offset being `0.0` to offset being `1.0`). The high two - bits being `0`, `1`, `2` or `3` mean none, pad, reflect and repeat - respectively. - - *None* means that offsets outside of the `[0.0, 1.0]` range map to - transparent black. - - *Pad* means that offsets below `0.0` and above `1.0` map to the colors that - `0.0` and `1.0` would map to. - - *Reflect* means that the offset mapping is reflected start-to-end, - end-to-start, start-to-end, etc. - - *Repeat* means that the offset mapping is repeated start-to-end, - start-to-end, start-to-end, etc. -- The low 6 bits of the blue value is the number register base, `NBASE`. -- The remaining bit (the `0x40` bit) of the blue value denotes the gradient - shape: `0` means a *linear gradient* and `1` means a *radial gradient*. - -The gradient has `NSTOPS` color/offset stops. The first stop has color -`CREG[CBASE+0]` and offset `NREG[NBASE+0]`, the second stop has color -`CREG[CBASE+1]` and offset `NREG[NBASE+1]`, and so on. - -The gradient also uses the six numbers from `NREG[NBASE-6]` to `NREG[NBASE-1]`, -which form an *affine transformation matrix* `[a, b, c; d, e, f]` such that -`a=NREG[NBASE-6], b=NREG[NBASE-5], ..., f=NREG[NBASE-1]`. This matrix maps from -graphic coordinate space (defined by the metadata's viewBox) 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: +Any update to the GFTM implicitly updates the GBTM. - dx = (a * px) + (b * py) + c - dy = (d * px) + (e * py) + f +The GFTM and GBTM are both initialized to the identity matrix `[1, 0, 0; 0, 1, +0]`. -The [appendix below](#appendix---gradient-transformation-matrices) gives -explicit formulae for the `[a, b, c; d, e, f]` affine transformation matrix for -common gradient geometry, such as a linear gradient defined by two points. +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. -At the time a gradient is used to fill a path, it is invalid for any of the -stop colors to itself be a gradient, or for any stop offset to be less than or -equal to a previous offset, or outside the range `[0, 1]`. +## SegRefs -### Colors +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`. -Color register values are always 32 bits, or 4 bytes. Colors in the instruction -byte stream can be encoded more compactly, and are encoded in either 1, 2, 3 or -4 bytes, depending on context. For example, some opcodes are followed by a 1 -byte color, others by a 2 byte color. There are two forms of 3 byte colors: -direct and indirect. +A SegRef also includes a `uint8` Segment Type describing the contents' +semantics. `0x00` means IconVG bytecode. All other values are reserved. -For a *1 byte encoding*, byte values in the range `[0, 125)` encode the `RGBA` -color where the red, green and blue values come from the base-5 encoding of -that byte value such that `0`, `1`, `2`, `3` and `4` map to `0x00`, `0x40`, -`0x80`, `0xC0` and `0xFF`. The alpha value is `0xFF`. For example, the color -`40:FF:C0:FF` can be encoded as `0x30`, as decimal `48` equals `(1*25 + 4*5 + -3)`. A byte value of `125`, `126` or `127` mean the colors `C0:C0:C0:C0`, -`80:80:80:80` and `00:00:00:00` respectively. Byte values in the range `[128, -192)` mean a color from the custom palette (indexed by that byte value minus -`128`). Byte values in the range `[192, 256)` mean the value of a `CREG` color -register (with `CREG` indexed by that byte value minus `192`). +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: -For a *2 byte encoding*, the red, green, blue and alpha values are all 4 bit -values which are extended to 8 bits by duplicating each nibble (or -equivalently, they are 4 bit color values interpolated to an 8 bit color -space). For example, the color `33:88:00:FF` can be encoded as `0x38 0x0F`. +- 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. -For a *3 byte direct encoding*, the red, green and blue values are all 8 bit -values. The alpha value is implicitly 255. For example, the color `30:66:07:FF` -can be encoded as `0x30 0x66 0x07`. +Throughout IconVG, integers are unsigned and little-endian unless otherwise +noted. -For a *4 byte encoding*, the red, green, blue and alpha values are all 8 bit -values. For example, the color `30:66:07:80` is simply `0x30 0x66 0x07 0x80`. -For a *3 byte indirect encoding*, the first byte is an integer value in the -range `[0, 255]` (denoted `T`) and the second and third bytes are each a 1 byte -encoded color (denoted `C0` and `C1`). The resultant color's red channel value -is given by: +## Registers - RESULTANT.RED = (((255-T) * C0.RED) + (T * C1.RED) + 128) / 255 +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]`. -rounded down to an integer value (the mathematical floor function), and -likewise for the green, blue and alpha channels. For example, if the custom -palette's third entry is a fully opaque orange, then `0x40 0x7F 0x82` encodes a -25% percent opaque orange: a blend of 75% times fully transparent (1 byte color -`0x7F`) and 25% times a fully opaque orange (1 byte color `0x82`). +VM state includes a selector register `SEL`. It is effectively a 6 bit integer, +as it indexes `REGS`. -It is valid for some encodings to yield a color value where the red, green or -blue value is greater than the alpha value, as this may be a gradient. If it -isn't a gradient, the subsequent rendering is undefined. +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. + + +### Registers' Low 32 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`. + + +### Registers' High 32 Bits +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: -### Palettes +- Bits `[32 ..= 39]` hold the `R` (red) channel. +- Bits `[40 ..= 47]` hold the `G` (green) channel. +- Bits `[48 ..= 55]` hold the `B` (blue) channel. +- Bits `[56 ..= 63]` hold the `A` (alpha) channel. -Rendering an IconVG graphic can be varied by a 64 color palette. For example, -an emoji graphic may be rendered 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. +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: -Rendering software should allow users to pass an optional 64 color palette. If -one isn't provided, the suggested palette (either [given in the -metadata](#mid-1-suggested-palette) or the default consisting entirely of -opaque black) should be used. Whichever palette ends up being used is -designated the *custom palette*. +- Bits `[32 ..= 39]` hold the `Blend`. +- Bits `[40 ..= 47]` hold the `ColRef0` Color Reference. +- Bits `[48 ..= 55]` hold the `ColRef1` 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: -Some user-given colors may be nonsensical as alpha-premultiplied colors, where -e.g. the red value is greater than the alpha value. Such colors are replaced by -opaque black and not re-interpreted as gradients. + Weight0 = 255 - Blend + Weight1 = Blend + Resultant.R = ((Weight0 * Color0.R) + (Weight1 * Color1.R) + 128) / 255 -Assigning names such as "skin", "hair" or "bow-tie" to the integer indexes of -that 64 color palette is out of scope of the IconVG format per se. +This is rounded down to an integer value (the mathematical floor function), and +likewise for the green, blue and alpha channels. -## Numbers +### Color References -Like colors, numbers are encoded in the instruction stream in either 1, 2 or 4 -bytes. Unlike colors, the encoding length is not determined by context (the -opcode). Instead, the low two bits of the first byte indicate the encoding -length. +There are three categories of 1-byte Color References: -If the least significant bit (the `0x01` bit) of the first byte is `0`, the -number is encoded in 1 byte. Otherwise, it is encoded in 2 or 4 bytes depending -on the second least significant bit (the `0x02` bit) of the first byte being -`0` or `1`. +- 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 another `REGS` 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. -### Natural Numbers +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]`. + + +### 128-Entry Built-In Palette + +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` + + +### Hit Testing + +Recall the `REGS` register bit assignments for blended colors: + +- Bits `[32 ..= 39]` hold the `Blend`. +- Bits `[40 ..= 47]` hold the `ColRef0` Color Reference. +- Bits `[48 ..= 55]` hold the `ColRef1` 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. + + +### 64-Entry Custom Palette + +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. -For a *1 byte encoding*, the remaining 7 bits form an integer value in the -range `[0, 1<<7)`. For example, `0x28` encodes the value `0x14` or, in decimal, -`20`. +A future IconVG version may allow Metadata to associate names like "skin", +"hair" or "bow-tie" to the integer indexes of the Custom Palette. -For a *2 byte encoding*, the remaining 14 bits, interpreted as little endian, -form an integer in the range `[0, 1<<14)`. For example, `0x59 0x83` encodes the -value `0x20D6` or, in decimal, `8406`. -For a *4 byte encoding*, the remaining 30 bits, interpreted as little endian, -form an integer in the range `[0, 1<<30)`. For example, `0x07 0x00 0x80 0x3F` -encodes the value `0xFE0_0001` or, in decimal, `266_338305`. +### Register Initialization +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. -### Real Numbers -The encoding of a real number resembles the encoding of a natural number. For -*1 and 2 byte encodings*, the decoded real number equals the decoded natural -number. For example, `0x28` encodes the real number `20.0`, just as it encodes -the natural number `20`. +## Numbers + +An IconVG file uses separate byte encodings for floating point, natural +(non-negative integer) and coordinate numbers. -For a *4 byte encoding*, the decoded natural number, in the range `[0, 1<<30)`, -is shifted left by 2, to make a `uint32` value that is a multiple of 4. The -decoded real number is the single-precision floating point number corresponding -to the IEEE 754 binary representation of that `uint32` (i.e. a reinterpretation -as a `float32`). The 30 effective bits partition as 1 sign bit, 8 exponent bits -and 21 explicit mantissa bits (the two least significant `float32` mantissa -bits are zeroed). For example, [`0x07 0x00 0x80 -0x3F`](https://play.golang.org/p/QAhFqPT9FYc) encodes the value +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`](https://go.dev/play/p/Wp_KptYcpMP) encodes the value `1.000000476837158203125`. -It is valid for the 4 byte encoding to represent infinities and `NaN`s, but if -not loaded into `LOD0` or `LOD1`, the subsequent rendering is undefined. +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. -### Coordinate Numbers +It is unusual but still valid for a natural or coordinate number to use a +longer encoding when an equivalent shorter encoding exists. -The encoding of a coordinate number resembles the encoding of a real number. -For 1 and 2 byte encodings, the decoded coordinate number equals `((R * -scale) - bias)`, where `R` is the decoded real number as above. The scale and -bias depends on the number of bytes in the encoding. -For a *1 byte encoding*, the scale is `1` and the bias is `64`, so that a 1 -byte coordinate ranges in `[-64, +64)` at integer granularity. For example, the -coordinate `7` can be encoded as `0x8E`. +### Natural Numbers -For a *2 byte encoding*, the scale is `1/64` and the bias is `128`, so that a 2 -byte coordinate ranges in `[-128, +128)` at `1/64` granularity. For example, -the coordinate `7.5` can be encoded as `0x81 0x87`. +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 *4 byte encoding*, the decoded coordinate number simply equals `R`. For -example, the coordinate `7.5` can also be encoded as [`0x03 0x00 0xF0 -0x40`](https://play.golang.org/p/QAhFqPT9FYc). +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`. -### Zero-to-One Numbers -A zero-to-one number is a real number that is typically in the range `[0, 1]`, -although it is valid for a value to be outside of that range. For example, -angles are expressed as a zero-to-one number: a fraction of a complete -revolution (360 degrees). Gradient stop offsets are another example. +### Coordinate Numbers -The encoding of a zero-to-one number resembles the encoding of a real number. -For 1 and 2 byte encodings, the decoded number equals `(R * scale)`, where `R` -is the decoded real number as above. The scale depends on the number of bytes -in the encoding. +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 real number (ranging up to 128) is scaled by -`1/120`. The denominator is `(2*2*2 * 3 * 5)`, so that 15 degrees (`2*π/24` -radians) can be encoded as `0x0A`. +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 real number (ranging up to 16384) is scaled by -`1/15120`. The denominator is `(2*2*2*2 * 3*3*3 * 5 * 7)`. For example, 40 -degrees (`2*π/9` radians) can be encoded as `0x41 0x1A`. +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 decoded zero-to-one number simply equals `R`. For -example, 1 degree (`2*π/360 radians`), or `0.002777777...`, can be approximated -by the encoding [`0x63 0x0B 0x36 0x3B`](https://play.golang.org/p/QAhFqPT9FYc). +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`](https://go.dev/play/p/Wp_KptYcpMP). ## Magic Identifier -An IconVG graphic starts with the four bytes `0x89 0x49 0x56 0x47`, which is -`"\x89IVG"`. +An IconVG graphic starts with the four bytes `0x8A 0x49 0x56 0x47`, which is +`"\x8aIVG"`. ## Metadata -The encoded metadata starts with a natural number ([see -above](#natural-numbers)) of the number of metadata chunks in the metadata, -followed by that many chunks. Each chunk starts with the length remaining in -the chunk (again, encoded as a natural number), not including the chunk length -itself. After that is a *MID* (Metadata Identifier) natural number, then -MID-specific data. 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 0 - ViewBox - -Metadata Identifier 0 means that the MID-specific data contains four coordinate -values ([see above](#coordinate-numbers)). These are the `minX`, `minY`, `maxX` -and `maxY` of the graphic's *viewBox*: its bounding rectangle in (scalable) -vector space. Note that these are abstract units and not necessarily 1:1 with -pixels. If this MID is not present, the viewBox defaults to `(-32, -32, +32, -+32)`. A viewBox is invalid if `(minX > maxX)` or if `(minY > maxY)` or if at -least one of those four values are infinite or a `NaN`. - - -### MID 1 - Suggested Palette - -Metadata Identifier 1 means that the MID-specific data contains a *suggested -palette*, e.g. to provide a default rendering of variable colors such as an -emoji's skin and hair. The suggested palette is encoded in at least one byte. -The low 6 bits of that byte form a number `N`. The high 2 bits denote the -palette color format: those high 2 bits being 0, 1, 2 or 3 mean 1, 2, 3 -(direct) or 4 byte colors ([see above](#colors)). The chunk then contains `N+1` -explicit colors, in that 1, 2, 3 or 4 byte encoding. A palette has exactly `64` -colors. The `(63 - N)` implicit colors of the suggested palette are set to -opaque black. A 1 byte color that refers to the custom palette or a `CREG` -color register resolves to opaque black. If this MID is not present, the -suggested palette consists entirely of opaque black, as black is always -fashionable. - - -## Opcodes - - -### Styling Opcodes - -Some op descriptions refer to an adjustment value, `ADJ`. That value is the low -three bits of the opcode, nominally in the range `[0, 8)`, although in practice -the range is `[0, 7)` as no `ADJ`-using opcode has the low three bits set. - -The first 128 opcodes set the selector registers. - -- Opcodes `0x00` to `0x3F` sets `CSEL` to the low 6 bits of the opcode. -- Opcodes `0x40` to `0x7F` sets `NSEL` to the low 6 bits of the opcode. - -The next 64 opcodes set the color or number registers. - -- Opcodes `0x80` to `0x86` sets `CREG[CSEL-ADJ]` to the 1 byte encoded color - following the opcode. -- Opcodes `0x88` to `0x8E` sets `CREG[CSEL-ADJ]` to the 2 byte encoded color - following the opcode. -- Opcodes `0x90` to `0x96` sets `CREG[CSEL-ADJ]` to the 3 byte direct encoded - color following the opcode. -- Opcodes `0x98` to `0x9E` sets `CREG[CSEL-ADJ]` to the 4 byte encoded color - following the opcode. -- Opcodes `0xA0` to `0xA6` sets `CREG[CSEL-ADJ]` to the 3 byte indirect encoded - color following the opcode. -- Opcodes `0x87`, `0x8F`, `0x97`, `0x9F` and `0xA7` sets `CREG[CSEL]` to the 1, - 2, 3 (direct), 4 and 3 (indirect) byte encoded color, following the opcode, - and then increments `CSEL` by `1`. -- Opcodes `0xA8` to `0xAE` sets `NREG[NSEL-ADJ]` to the real number following - the opcode. -- Opcodes `0xB0` to `0xB6` sets `NREG[NSEL-ADJ]` to the coordinate number - following the opcode. -- Opcodes `0xB8` to `0xBE` sets `NREG[NSEL-ADJ]` to the zero-to-one number - following the opcode. -- Opcode `0xAF`, `0xB7` and `0xBF` sets `NREG[NSEL]` to the real, coordinate - and zero-to-one number following the opcode, and then increments `NSEL` by - `1`. - -The remaining opcodes are miscellaneous. - -- Opcodes `0xC0` to `0xC6` switch to the drawing mode, and are followed by two - coordinates that is the path's starting location. In effect, there is an - implicit `M` (absolute moveto) drawing op. `CREG[CSEL-ADJ]`, either a flat - color or a gradient, will fill the path once it is complete. -- Opcode `0xC7` sets the Level of Detail bounds `LOD0` and `LOD1` to the two - real numbers following the opcode. - -All other opcodes are reserved. - - -### Drawing Opcodes - -The drawing model is based on [SVG path -data](https://www.w3.org/TR/SVG/paths.html#PathData) and this description -re-uses SVG's one-letter mnemonics: - -- `M` means absolute moveto, -- `m` means relative moveto, -- `L` means absolute lineto, -- `l` means relative lineto, -- `H` means absolute horizontal lineto, -- etc. - -Upper and lower case mean absolute and relative coordinates. The upper case -mnemonics of the SVG operations used in IconVG's drawing mode are: `M`, `Z`, -`L`, `H`, `V`, `C`, `S`, `Q`, `T`, `A`. - -IconVG differs from SVG with multiple consecutive moveto ops. SVG treats all -but the first one as lineto ops. IconVG treats them all as moveto ops. - -The first 224 opcodes, those in the range `[0x00, 0xDF]`, come in contiguous -groups of 16 or 32. For example, there are 16 `Q` (absolute quadratic Bézier -curveto) opcodes, from `0x60` to `0x6F`. Those opcodes' meaning differ only in -their repeat count `RC`: how often that drawing operation is repeated. The -lowest valued opcode has a repeat count of 1, the next lowest has a repeat -count of 2 and so on. For example, the opcode `0x68` means 9 consecutive `Q` -drawing ops. - -- Opcodes `0x00` to `0x1F` means `RC` consecutive `L` ops, for `RC` in `[1, - 32]`. The opcode is followed by `(2 * RC)` coordinates, `RC` sets of `(x, - y)`. -- Opcodes `0x20` to `0x3F` are like the previous paragraph, except `L` - (absolute) becomes `l` (relative). -- Opcodes `0x40` to `0x4F` means `RC` consecutive `T` ops, for `RC` in `[1, - 16]`. The opcode is followed by `(2 * RC)` coordinates, `RC` sets of `(x, - y)`. -- Opcodes `0x50` to `0x5F` are like the previous paragraph, except `T` - (absolute) becomes `t` (relative). -- Opcodes `0x60` to `0x6F` means `RC` consecutive `Q` ops, for `RC` in `[1, - 16]`. The opcode is followed by `(4 * RC)` coordinates, `RC` sets of `(x1, - y1, x, y)`. -- Opcodes `0x70` to `0x7F` are like the previous paragraph, except `Q` - (absolute) becomes `q` (relative). -- Opcodes `0x80` to `0x8F` means `RC` consecutive `S` ops, for `RC` in `[1, - 16]`. The opcode is followed by `(4 * RC)` coordinates, `RC` sets of `(x2, - y2, x, y)`. -- Opcodes `0x90` to `0x9F` are like the previous paragraph, except `S` - (absolute) becomes `s` (relative). -- Opcodes `0xA0` to `0xAF` means `RC` consecutive `C` ops, for `RC` in `[1, - 16]`. The opcode is followed by `(6 * RC)` coordinates, `RC` sets of `(x1, - y1, x2, y2, x, y)`. -- Opcodes `0xB0` to `0xBF` are like the previous paragraph, except `C` - (absolute) becomes `c` (relative). -- Opcodes `0xC0` to `0xCF` means `RC` consecutive `A` ops, for `RC` in `[1, - 16]`. The opcode is followed by `(6 * RC)` numbers, `RC` sets of `(rx, ry, - xAxisRotation, flags, x, y)`. The `rx`, `ry`, `x` and `y` numbers are - coordinates. The `xAxisRotation` number is an angle (a zero-to-one number - being a fraction of 360 degrees). The flags are encoded as a natural number. - The `0x01` bit of the decoded natural number is the large-arc-flag and the - `0x02` bit is the sweep-flag. -- Opcodes `0xD0` to `0xDF` are like the previous paragraph, except `A` - (absolute) becomes `a` (relative). - -The remaining opcodes are miscellaneous. - -- Opcode `0xE0` is reserved. (A future version of IconVG may use this opcode to - mean the same as `0xE1` without the one `z` op, which will matter for stroked - paths). -- Opcode `0xE1` means one `z` op and then end the path: fill the path with the - color chosen when we switched to the drawing mode, and switch back to the - styling mode. (The `Z` and `z` ops are equivalent). -- Opcode `0xE2` means one z op and then an implicit `M` op to the `(x, y)` - coordinates after the opcode. -- Opcode `0xE3` means one z op and then an implicit `m` op to the `(x, y)` - coordinates after the opcode. -- Opcodes `0xE4` and `0xE5` are reserved. (A future version of IconVG may use - these for `M` and `m` ops, if we allow stroked paths). -- Opcode `0xE6` means one `H` op to the `x` coordinate after the opcode. -- Opcode `0xE7` means one `h` op to the `x` coordinate after the opcode. -- Opcode `0xE8` means one `V` op to the `y` coordinate after the opcode. -- Opcode `0xE9` means one `v` op to the `y` coordinate after the opcode. - -All other opcodes are reserved. - -These opcode descriptions all assume that the Level of Detail constraint ([see -above](#level-of-detail)) is satisfied. If not, the opcodes and their variable -length data are consumed, but no further action is taken (other than leaving -drawing mode). - - -## Example +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 - ViewBox + +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 - Suggested Palette + +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 + +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]`. + + +## Geometry Ops + +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)`. + + +### Ellipse and Parallelogram Ops + +- 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-` and `B`. +- The 2nd segment's control points are `B`, `B+`, `C-` and `C`. +- The 3rd segment's control points are `C`, `C+`, `D-` and `D`. +- The 4th segment's control points are `D`, `D+`, `A-` and `A`. + +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"](https://nigeltao.github.io/blog/2020/three-points-define-ellipse.html). + +- The `0x30` Quarter Ellipse op adds only the 1st segment to the Current Path, + moving the Pen Position to `B`. +- The `0x31` Half Ellipse op adds the 1st and 2nd segments, moving to `C`. +- The `0x32` Three Quarter Ellipse op adds the 1st, 2nd and 3rd segments, + moving to `D`. +- The `0x33` Full Ellipse op adds all four segments and leaves the Pen Position + unchanged at `A`. + + +### ClosePathMoveTo Op + +- 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. + + +## Miscellaneous Ops + +- Opcode `0x36` is a 2-byte op that adds (modulo 64) the second byte to `SEL`. + For example, the byte sequence `0x36 0x02` adds 2 to `SEL`. +- Opcode `0x37` is a 1-byte NOP (no operation; do nothing). + + +## Control Flow Ops + +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. + + +### End Of Bytecode + +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. + + +### Jump Ops + +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). The `JumpCount` is + followed by a natural number `FeaturesNeeded`. 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). The `JumpCount` is + followed by two floating point numbers, `LOD0` and `LOD1`. The jump is taken + unless the rasterization's height in pixels `H` 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). + + +### Feature Detection + +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. + + +### Return Op + +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. + + +### Call Ops + +- 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. + + +## Register Ops + +- Opcodes `[0x40 ..= 0x4F]` set the low 32 bits of `REGS[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 of `REGS[SEL + LOW4]`. The low + 32 bits are set to zero. The opcode is followed by 4 bytes. +- Opcodes `[0x60 ..= 0x6F]` set all 64 bits of `REGS[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 decrements `SEL` by `(LOW4 + 2)`. The opcode + is followed by `(8 * (LOW4 + 2))` bytes, which are then copied 8 bytes at a + time to `REGS[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. + + +## Fill Ops + +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, resolving + `REGS[SEL + LOW4]`. +- Opcodes `[0x90 ..= 0x9F]` fill with a linear gradient. It is followed by a + Gradient Configuration byte and then three coordinate numbers `Na`, `Nb` and + `Nc`. The `Nd`, `Ne` and `Nf` 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 coordinate numbers `Na`, `Nb`, `Nc`, + `Nd`, `Ne` and `Nf`. + +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 below `0` and above `1` map to + transparent black. +- `0x01` Pad means that stop positions below `0` and above `1` map to the + colors that `0` and `1` 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. + + +### Gradients + +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](#appendix---gradient-transformation-matrices) 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. + + +### Gradient Example + +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"](#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](test/data/gradient.iconvg.disassembly) example +file. The [test/data](/test/data) directory holds other gradient examples. + + +## Reserved Ops + +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-incrementing `SEL` when `LOW4` 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. + + +## IconVG Example An ASCII art rasterization of Material Design's "action/info" icon: @@ -520,177 +817,198 @@ The PNG forms at various sizes: 36 x 36: 321 bytes 48 x 48: 412 bytes -The IconVG form is 73 bytes (and could be smaller still at 63 bytes if we're -willing to quantize `11.05` and `8.95` to `11.046875 = (11 + 3/64)` and -`8.953125 = (8 + 61/64)`; neither `11.05` or `8.95` are exactly representable -as a `float32` anyway): +The IconVG form is 36 bytes: - 89 49 56 47 02 0a 00 50 50 b0 b0 c0 80 58 a0 cf - cc 30 c1 58 58 cf cc 30 c1 58 80 91 37 33 0f 41 - a8 a8 a8 a8 37 33 0f c1 a8 58 80 cf cc 30 41 58 - 80 58 e3 84 bc e7 78 e8 7c e7 88 e9 98 e3 80 60 - e7 78 e9 78 e7 88 e9 88 e1 + 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 +The annotated disassembly is below. Note that the IconVG ViewBox ranges from `-24` to `+24` while the SVG viewBox ranges from `0` to `48`. - 89 49 56 47 IconVG Magic identifier - 02 Number of metadata chunks: 1 - 0a Metadata chunk length: 5 - 00 Metadata Identifier: 0 (viewBox) - 50 -24 - 50 -24 - b0 +24 - b0 +24 - c0 Start path, filled with CREG[CSEL-0]; M (absolute moveTo) - 80 +0 - 58 -20 - a0 C (absolute cubeTo), 1 reps - cf cc 30 c1 -11.049999 - 58 -20 - 58 -20 - cf cc 30 c1 -11.049999 - 58 -20 - 80 +0 - 91 s (relative smooth cubeTo), 2 reps - 37 33 0f 41 +8.950001 - a8 +20 - a8 +20 - a8 +20 - s (relative smooth cubeTo), implicit - a8 +20 - 37 33 0f c1 -8.950001 - a8 +20 - 58 -20 - 80 S (absolute smooth cubeTo), 1 reps - cf cc 30 41 +11.049999 - 58 -20 - 80 +0 - 58 -20 - e3 z (closePath); m (relative moveTo) - 84 +2 - bc +30 - e7 h (relative horizontal lineTo) - 78 -4 - e8 V (absolute vertical lineTo) - 7c -2 - e7 h (relative horizontal lineTo) - 88 +4 - e9 v (relative vertical lineTo) - 98 +12 - e3 z (closePath); m (relative moveTo) - 80 +0 - 60 -16 - e7 h (relative horizontal lineTo) - 78 -4 - e9 v (relative vertical lineTo) - 78 -4 - e7 h (relative horizontal lineTo) - 88 +4 - e9 v (relative vertical lineTo) - 88 +4 - e1 z (closePath); end path + 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](/test/data) directory holds these files and other examples. ## Appendix - Gradient Transformation Matrices -This appendix derives the affine transformation matrices `[a, b, c; d, e, f]` +This appendix derives the Nominal Gradient Matrices `[Na, Nb, Nc; Nd, Ne, Nf]` for linear, circular and elliptical gradients. ### Linear Gradients -For a linear gradient from `(x1, y1)` to `(x2, y2)`, let `dx = (x2 - x1)` and -`dy = (y2 - y1)`. In gradient coordinate space, the `y`-coordinate is ignored, -so the transformation matrix simplifies to `[a, b, c; 0, 0, 0]`. It satisfies -the three simultaneous equations: +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: - a*(x1 ) + b*(y1 ) + c = 0 (eq L.0) - a*(x1+dy) + b*(y1-dx) + c = 0 (eq L.1) - a*(x1+dx) + b*(y1+dy) + c = 1 (eq L.2) + 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: - a*dy - b*dx = 0 - a*dx + b*dy = 1 + Na*Δy - Nb*Δx = 0 + Na*Δx + Nb*Δy = 1 So that - a*dy*dy - b*dx*dy = 0 - a*dx*dx + b*dx*dy = dx + Na*Δy*Δy - Nb*Δx*Δy = 0 + Na*Δx*Δx + Nb*Δx*Δy = Δx Overall: - a = dx / (dx*dx + dy*dy) - b = dy / (dx*dx + dy*dy) - c = -a*x1 - b*y1 - d = 0 - e = 0 - f = 0 + Na = Δx / (Δx*Δx + Δy*Δy) + Nb = Δy / (Δx*Δx + Δy*Δy) + Nc = -a*Px1 - b*Py1 + Nd = 0 + Ne = 0 + Nf = 0 + + +### Inverse Linear Gradients + +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`. ### Circular Gradients -For a circular gradient with center `(cx, cy)` and radius vector `(rx, ry)`, -such that `(cx+rx, cy+ry)` is on the circle, let +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(rx*rx + ry*ry) + r = math.Sqrt(Prx*Prx + Pry*Pry) -The transformation matrix maps `(cx, cy)` to `(0, 0)`, maps `(cx+r, cy)` to -`(1, 0)` and maps `(cx, cy+r)` to `(0, 1)`. Solving those six simultaneous +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: - a = +1 / r - b = +0 / r - c = -cx / r - d = +0 / r - e = +1 / r - f = -cy / r + Na = +1 / r + Nb = +0 / r + Nc = -Pcx / r + Nd = +0 / r + Ne = +1 / r + Nf = -Pcy / r ### Elliptical Gradients -For an elliptical gradient with center `(cx, cy)` and axis vectors `(rx, ry)` -and `(sx, sy)`, such that `(cx+rx, cx+ry)` and `(cx+sx, cx+sy)` are on the -ellipse, the transformation matrix satisfies the six simultaneous equations: +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: - a*(cx ) + b*(cy ) + c = 0 (eq E.0) - a*(cx+rx) + b*(cy+ry) + c = 1 (eq E.1) - a*(cx+sx) + b*(cy+sy) + c = 0 (eq E.2) - d*(cx ) + e*(cy ) + f = 0 (eq E.3) - d*(cx+rx) + e*(cy+ry) + f = 0 (eq E.4) - d*(cx+sx) + e*(cy+sy) + f = 1 (eq E.5) + 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: - a*rx + b*ry = 1 - a*sx + b*sy = 0 + Na*Prx + Nb*Pry = 1 + Na*Psx + Nb*Psy = 0 Solving these two simultaneous equations yields: - a = +sy / (rx*sy - sx*ry) - b = -sx / (rx*sy - sx*ry) + Na = +Psy / (Prx*Psy - Psx*Pry) + Nb = -Psx / (Prx*Psy - Psx*Pry) Re-arranging `E.0` yields: - c = -a*cx - b*cy + Nc = -Na*Pcx - Nb*Pcy -Similarly for `d`, `e` and `f` so that, overall: +Similarly for `Nd`, `Ne` and `Nf` so that, overall: - a = +sy / (rx*sy - sx*ry) - b = -sx / (rx*sy - sx*ry) - c = -a*cx - b*cy - d = -ry / (rx*sy - sx*ry) - e = +rx / (rx*sy - sx*ry) - f = -d*cx - e*cy + 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 `rx = r`, `ry = 0`, `sx = 0` and `sy = r` then this simplifies to -the circular gradient transformation matrix formula ([see -above](#circular-gradients)). +Note that if `Prx = r`, `Pry = 0`, `Psx = 0` and `Psy = r` then this simplifies +to the Circular Gradients formulae [above](#circular-gradients). --- -Updated on March 2021. +Updated on December 2021.