Features:
- written in C
- allocation free (single instance static memory)
- crafted for web assembly / emscripten
- also embeddable in native code
- quite fast (decoding throughput >150 MB/s)
- small wasm binary (~11 kB)
- small memory footprint (<<1 MB, depending on
MAX_WIDTH
)
Currently wasm engines differ alot in supported wasm features.
To still support a wide variety of engines, this module is coded in vanilla C and
only uses the bulk-memory
feature (optionally).
The decoder can be used from other native C/C++ projects by simply including decoder.cpp
.
Still for native embedding there are a few things to consider first:
-
single instance / static memory
The parser state is a single statically allocated struct. While this is a perfect fit for isolated wasm module instances, it is prolly not what you want in a C or C++ project, unless for a simple cmdline converter tool. Without code modifications decoding is limited to one parser state / one image at a time. (This is easy fixable by introducing state indirections at the function interfaces, but beyond the scope here.) -
code optimizations
The decoder loop is a highly optimized byte-by-byte loop in vanilla C. While this also runs fast natively, things like SIMD extensions would give another quite remarkable boost, but do not work reliable in wasm yet.
- wasm-SIMD, currently engines have no to lousy support with bad performance
(see dysfunctional proof of concept in
decoder-simd.cpp
) - shared memory, currently shifting too much in browsers
- wasm-threading, still not fully landed in wasm, depends on stable shared memory interfaces
To use the decoder from other non javascript wasm environments or natively,
you gonna need to hook into the following API.
For reference usage and how to build a full image decoder from the sixel band handling,
see the implementation for javascript under src/WasmDecoder.ts
.
Important compile time settings (see build.sh to adjust):
CHUNK_SIZE
- max amount of chunk bytesPALETTE_SIZE
- max colors in the paletteMAX_WIDTH
- max band width (excess pixels to the right are truncated)
Exported symbols:
void* get_state_address()
Void pointer to the staticParserState
struct in WASM memory.
Properties of interest (indexed in 32bit):- 1: fill color as ABGR32 (as given by
init
) - 2: width+4 in M2, else 0
- 3: height in M2, else 0
- 4: raster numerator (unmodified) in M2, else 0
- 5: raster denominator (unmodified) in M2, else 0
- 6: raster width (unmodified) in M2, else 0
- 7: raster height (unmodified)in M2, else 0
- 8: truncate (as given by
init
) - 9: image level (L0 - undecided, L1 - level 1, L2 - level 2)
- 10: operation mode (M0 - undecided, M1 - level 1/2 !truncate, M2 - level 2 truncating)
- 11: palette length
- 1: fill color as ABGR32 (as given by
void* get_chunk_address()
Void pointer toParserState.chunk
byte array (max size ofCHUNK_SIZE
). Used to load image data to be processed bydecode
.void* get_p0_address()
Void pointer to first pixel line p0 the band. The other pixel lines p1 - p5 start atget_p0_address() + (MAX_WIDTH + 4) * line_idx
. Used to grab pixel data when a band was finished.void* get_palette_address()
Void pointer toParserState.palette
ABGR32 array (max size ofPALETTE_SIZE
). Used to read/write palette colors.void init(int fill_color, unsigned int palette_limit, int truncate)
Initialize decoder for new image. Must be called before any decoding happens.void decode(int start, int end)
Decode data loaded intoParserState.chunk[start .. end]
(right exclusive).int current_width()
Return the cursor advance of the current band in M1 mode, or width in M2 mode. This is needed to properly construct the full image at the end of decoding, in case the data did not finish with LF.int current_width()
M1 mode only - return the current lowermost pixel position touched by a sixel of the current band. This is needed to properly construct the full image with proper height for the current band, in case the data did not finish with LF.
Needed callbacks:
int mode_parsed(int mode)
Called once early during decoding when the parser settled the operation mode. The operation mode will be settled by the decoder on reading a valid raster attributes command or any other sixel command or sixel data bytes. Used to announce the operation mode and potential raster attributes for further preparation steps. Return 0 to continue, 1 to abort further processing.int handle_band(int width)
Called upon finishing decoding of a sixel band (on LF).width
is either the raster width (M2 mode), or the max cursor advance (both clamped tomaxWidth
). Used to copy pixel in p0 .. p5, before continuing with the next band. Return 0 to continue, 1 to abort further processing.
- The data to be digested by this decoder should only be the "Picture Definition" part of a SIXEL escape sequence, as denoted by the spec. Any other data beyond that is likely to screw up the image creation. In particular this means, that the decoder should run behind a terminal escape sequence, that is capable of proper DEC style DCS parsing (also handling spurious ESC, SUB and C1 codes).
- Raster or pixel ratio definitions are not dealt with beside width and height (if
truncate
is set). Raster attributes are exposed unmodified in 0 .. 2^31-1 (as read from data), and can be used to postprocess pixel data as needed. - With
truncate
set ininit
, the decoder will truncate the image to given raster dimensions. This does not apply to level 1 images without any raster attributes. While truncation to raster dimensions on decoder side is not spec-conform, it is the expected data format created by a spec-conform encoder. - If
truncate
is not set, the width will be derived from cursor advance to the right, clamped toMAX_WIDTH-4
. In this mode,current_height
is reported as lowermost pixel position touched by sixels. Furthermore in this mode the height is not limited by any means, thus decoding may run forever. Use some sort of accounting duringhandle_band
to spot malformed data or excessive memory usage, especially when dealing with data streams. - Palette colors are applied immediately to sixels (printer mode), there is no terminal-like indexed mode. While this is in line with the spec, it does not allow to mimick the palette behavior of older terminals (e.g. palette animations are not possible).
- The decoder unconditionally strips the 8th bit, mapping all data bytes in 7-bit space. While the spec defines this only as error recovery strategy for GR codes, the decoder also does this for C1, which might lead to sixel command interpretation from spurious C1 codes. Note that C1 never should appear in sixel data, if used behind a proper escape sequence parser.
- Other than stated in the spec, the decoder does not error on low C0 codes, instead silently ignores them. Again a proper escape sequence parser will filter / act upon those.
- The repeat count gets not limited/clamped to 32767. The digits are parsed in signed int32 for
performance reasons, and converted to unsigned before used as repeat count. While the counter
will show weird behavior above 2^31-1 (counting backwards), it should not be possible to overflow
the pixel arrays with malicious data (separately tested against
MAX_WIDTH
before any painting).
There are probably more deviations from the SIXEL spec not listed here.
Beta. The code is tested with unit/regression tests from the JS integration.