Home

Awesome

Sixel images in Javascript.

SIXEL image decoding / encoding library for node and the browser.

Decoding

For decoding the library provides a stream decoder, that can either be used directly, or with the following convenient functions:

Decoder

The decoder uses webassembly for data decoding (see /wasm for the webassembly parts). It exposes the following properties:

Encoding

For encoding the library provides the following properties:

Convenient Properties

Furthermore the library exposes some general purpose properties:

Installation

Install the library with npm install sixel.

For direct usage in the browser, see under "browser bundles" below.

Examples and browser demo

See the example files in /examples for decoding/encoding in nodejs. Note that the examples and the browser demo are not part of the npm package, clone the repo and run npm install if you want to see them in action.

Decoding can also be tested in the browser after npm start under localhost:8080.

Encoding can be tested in a SIXEL capable terminal with img2sixel.js, e.g.

$> node img2sixel.js -p16 http://leeoniya.github.io/RgbQuant.js/demo/img/bluff.jpg

Benchmarks

Performance is measured for typical actions based on 9-bit palette image: test image

The test image repeats the palette image 6 times to form a 640x480 image with 512 colors. The unusual (and not spec conform) high number of colors was chosen to explicit test for this as an upper bound.

Results:

   Context "lib/index.benchmark.js"
      Context "testimage"
         Context "decode"
            Case "decode" : 20 runs - average runtime: 0.77 ms
            Case "decodeString" : 20 runs - average runtime: 1.76 ms
         Context "encode"
            Case "sixelEncode" : 20 runs - average runtime: 21.52 ms
      Context "decode - testfiles"
         Case "test1_clean.sixel" : 20 runs - average runtime: 4.36 ms
         Case "test1_clean.sixel" : 20 runs - average throughput: 144.83 MB/s
         Case "test2_clean.sixel" : 20 runs - average runtime: 1.96 ms
         Case "test2_clean.sixel" : 20 runs - average throughput: 161.27 MB/s
         Case "sampsa_reencoded_clean.six" : 20 runs - average runtime: 4.36 ms
         Case "sampsa_reencoded_clean.six" : 20 runs - average throughput: 148.78 MB/s
         Case "FullHD 12bit noise" : 20 runs - average runtime: 50.70 ms
         Case "FullHD 12bit noise" : 20 runs - average throughput: 306.03 MB/s
         Case "640x480 9bit tiles" : 20 runs - average runtime: 0.68 ms
         Case "640x480 9bit tiles" : 20 runs - average throughput: 148.33 MB/s

Decoder usage

For casual usage and when you have the full image data at hand, you can use the convenient functions decode or decodeAsync.

Example (Typescript):

import { decode, decodeAsync, IDecoderOptions } from 'sixel';

// some options
const OPTIONS: IDecoderOptions = {
    memoryLimit: 65536 * 256, // limit pixel memory to 16 MB (2048 x 2048 pixels)
    ...
};

// in nodejs or web worker context
const result = decode(some_data, OPTIONS);
someRawImageAction(result.data32, result.width, result.height);

// in browser main context
decodeAsync(some_data, OPTIONS)
  .then(result => someRawImageAction(result.data32, result.width, result.height));

These functions are much easier to use than the stream decoder, but come with a performance penalty of ~25% due to bootstrapping into the wasm module everytime. Do not use them, if you have multiple images to decode. Also they cannot be used for chunked data.

For more advanced use cases with multiple images or chunked data, use the stream decoder directly.

Example (Typescript):

import { Decoder, DecoderAsync, IDecoderOptions } from 'sixel';

// some options
const OPTIONS: IDecoderOptions = {
    memoryLimit: 65536 * 256, // limit pixel memory to 16 MB (2048 x 2048 pixels)
    ...
};

// in nodejs or web worker context
const decoder = new Decoder(OPTIONS);
// in browser main context
const decoderPromise = DecoderAsync(OPTIONS).then(decoder => ...);

// and later on:
for (image of images) {
    // initialize for next image with defaults
    // for a more terminal like behavior you may want to override default settings
    // with init arguments, e.g. set fillColor to BG color / reflect palette changes
    decoder.init();

    // for every incoming chunk call decode
    for (chunk of image.data_chunks) {
        decoder.decode(chunk);
        // optional: check your memory limits
        if (decoder.memoryUsage > YOUR_LIMIT) {
            // the decoder is meant to be resilient for exceptional conditions
            // and can be re-used after calling .release (if not, please file a bug)
            // (for simplicity example exits whole loop)
            decoder.release();
            throw new Error('dont like your data, way too big');
        }
        // optional: grab partial data (useful for slow transmission)
        somePartialRawImageAction(decoder.data32, decoder.width, decoder.height);
    }

    // image finished, grab pixels and dimensions
    someRawImageAction(decoder.data32, decoder.width, decoder.height);

    // optional: release held pixel memory
    decoder.release();
}

Note on decoder memory handling

The examples above all contain some sort of memory limit notions. This is needed, because sixel image data does not strictly announce dimensions upfront, instead incoming data may implicitly expand image dimensions. While the decoder already limits the max width of an image with a compile time setting, there is no good way to limit the height of an image (can run "forever").

To not run into out of memory issues the decoder respects an upper memory limit for the pixel array. The default limit is set rather high (hardcoded to 128 MB) and can be adjusted in the decoder options as memoryLimit in bytes. You should always adjust that value to your needs.

During chunk decoding the memory usage can be further tracked with memoryUsage. Other than memoryLimit, this value also accounts the static memory taken by the wasm instance, thus is slightly higher and closer to the real usage of the decoder. Note that the decoder will try to pre-allocate the pixel array, if it can derive the dimensions early, thus memoryUsage might not change anymore for subsequent chunks after an initial jump. If re-allocation is needed during decoding, the decoder will hold up to twice of memoryLimit for a short amount of time.

During decoding the decoder will throw an error, if the needed pixel memory exceeds memoryLimit.

Between multiple images the decoder will not free the pixel memory of the previous image. This is an optimization to lower allocation and GC pressure. Call release after decoding to explicitly free the pixel memory.

Rules of thumb regarding memory:

Package format and browser bundles

The node package comes as CommonJS and can be used as usual. An ESM package version for nodejs is planned for a later release.

For easier usage in the browser the package contains several prebuilt bundles under /dist:

The browser bundles come in UMD and ESM flavors. At the current stage the ESM builds are mostly untested (treat them as alpha, bug reports are more than welcome). Note that the UMD bundles export the symbols under the name sixel.

Some usage examples:

Status

Beta.

Automatically tested on nodejs 12, 14 and 16. Manually tested on recent versions of Chrome, Firefox and Webkit.

References

While being quite common in the DEC ecosystem in the 80s (even used for printer protocols), SIXEL references are very limited these days. The closest to a specification we have can be found in the Video Systems Reference Manual (DEC STD 070, p. 908-933). Also see Sixel Graphics on vt100.net, which gives a quick overview. For implementation the old usenet article "All About SIXELs" was very helpful.