Skip to content

Commit b6b107c

Browse files
committedAug 28, 2021
multiple changes:
- add DEFAULT_FOREGROUND color in Color.ts - removed DimensionDecoder (now part of wasm code) - removed SixelDecoderL2 (now part of wasm code) - fix prolly undefined palette color in encoder - wasm decoder: - changed to handle only one band at a time - static fixed memory with upper max band width - optimize with bulk-memory - added normal M1 for level 1 images or non truncating - cleanup emscripten install & wam build scripts - python wasmer example - wasm-simd proof of concept implementation - moved WasmDecoder.ts to Decoder.ts - Decoder.ts prepared as new main decoder - convenient decode functions - add new decoder to benchmarks - cleanup imports/export in index.ts - better build scripts: - remove the need for json imports - add UMD/ESM bundles for browser/webpack
1 parent 38af1e1 commit b6b107c

32 files changed

+2270
-1504
lines changed
 

‎.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
/.nyc_output
77
/benchmark
88
/.vscode
9-
/wasm/sixel.wasm
10-
/src/wasm.json
9+
/wasm/decoder.wasm
10+
/wasm/settings.json
11+
/src/wasm.ts
12+
/emsdk

‎.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ src/*.benchmark.*
1616
src/*.test.*
1717
img2sixel.js
1818
.vscode
19+
lib-esm
20+
emsdk

‎README.md

Lines changed: 162 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,6 @@ Properties of `DefaultDecoder`:
7373
meaning as in the constructor, explicit setting it to 0 will leave non encoded pixels unaltered (pixels, that were not colored in the SIXEL data). This can be used for a transparency like effect (background/previous pixel value will remain). Returns the altered `target`.
7474

7575

76-
#### WasmDecoder
77-
78-
The `WasmDecoder` is a level 2 only decoder written in C and compiled to WebAssembly. While it can decode image data much faster (see benchmarks below), it imposes several usage restrictions:
79-
80-
- limited to 1536 x 1536 pixels and 4096 palette colors (compile time settings)
81-
- level 2 only, needs proper pixel dimensions (can be obtained from raster attributes with `DimensionDecoder`)
82-
- always truncates images to pixel dimensions (not spec conform)
83-
84-
Other than the default decoder, `WasmDecoder` is meant to be re-used with follow-up images, which lowers the need to spawn webassembly instances.
85-
86-
TODO: document properties + canUsewasm + DimensionDecoder
87-
88-
8976
### Encoding
9077

9178
For encoding the library provides the following properties:
@@ -194,8 +181,169 @@ Results:
194181
TODO...
195182

196183

184+
185+
### Decoder usage
186+
187+
For casual usage and when you have the full image data at hand,
188+
you can use the convenient functions `decode` or `decodeAsync`.
189+
190+
_Example (Typescript):_
191+
```typescript
192+
import { decode, decodeAsync, ISixelDecoderOptions } from 'sixel';
193+
194+
// some options
195+
const OPTIONS: ISixelDecoderOptions = {
196+
memoryLimit: 65536 * 256, // limit pixel memory to 16 MB (2048 x 2048 pixels)
197+
...
198+
};
199+
200+
// in nodejs or web worker context
201+
const result = decode(some_data, OPTIONS);
202+
someRawImageAction(result.data32, result.width, result.height);
203+
204+
// in browser main context
205+
decodeAsync(some_data, OPTIONS)
206+
.then(result => someRawImageAction(result.data32, result.width, result.height));
207+
```
208+
209+
These functions are much easier to use than the stream decoder,
210+
but come with a performance penalty of ~25% due to bootstrapping into
211+
the wasm module everytime. Do not use them, if you have multiple images to decode.
212+
Also they cannot be used for chunked data.
213+
214+
For more advanced use cases with multiple images or chunked data,
215+
use the stream decoder directly.
216+
217+
_Example (Typescript):_
218+
```typescript
219+
import { Decoder, DecoderAsync, ISixelDecoderOptions } from 'sixel';
220+
221+
// some options
222+
const OPTIONS: ISixelDecoderOptions = {
223+
memoryLimit: 65536 * 256, // limit pixel memory to 16 MB (2048 x 2048 pixels)
224+
...
225+
};
226+
227+
// in nodejs or web worker context
228+
const decoder = new Decoder(OPTIONS);
229+
// in browser main context
230+
const decoder = DecoderAsync(OPTIONS);
231+
232+
for (image of images) {
233+
// initialize for next image with defaults
234+
// for a more terminal like behavior you may want to override default settings
235+
// with init arguments, e.g. set fillColor to BG color / reflect palette changes
236+
decoder.init();
237+
238+
// for every incoming chunk call decode
239+
for (chunk of image.data_chunks) {
240+
decoder.decode(chunk);
241+
// optional: check your memory limits
242+
if (decoder.memoryUsage > YOUR_LIMIT) {
243+
// the decoder is meant to be resilient for exceptional conditions
244+
// and can be re-used after calling .release (if not, please file a bug)
245+
// (for simplicity example exists whole loop)
246+
decoder.release();
247+
throw new Error('dont like your data, way too big');
248+
}
249+
// optional: grab partial data (useful for slow transmission)
250+
somePartialRawImageAction(decoder.data32, decoder.width, decoder.height);
251+
}
252+
253+
// image finished, grab pixels and dimensions
254+
someRawImageAction(decoder.data32, decoder.width, decoder.height);
255+
256+
// optional: release held pixel memory
257+
decoder.release();
258+
}
259+
```
260+
261+
262+
__Note on decoder memory handling__
263+
264+
The examples above all contain some sort of memory limit notions. This is needed,
265+
because sixel image data does not strictly announce dimensions upfront,
266+
instead incoming data may implicitly expand image dimensions. While the decoder already
267+
limits the max width of an image with a compile time setting,
268+
there is no good way to limit the height of an image (can run "forever").
269+
270+
To not run into out of memory issues the decoder respects an upper memory limit for the pixel array.
271+
The default limit is set rather high (hardcoded to 128 MB) and can be adjusted in the decoder options
272+
as `memoryLimit` in bytes. You should always adjust that value to your needs.
273+
274+
During chunk decoding the memory usage can be tracked with `memoryUsage`. Other than `memoryLimit`,
275+
this value also accounts the static memory taken by the wasm module, thus is slightly higher and
276+
closer to the real usage of the decoder. Note that the decoder will try to pre-allocate the pixel array,
277+
if it can derive the dimensions early, thus `memoryUsage` might not change anymore for subsequent
278+
chunks after an initial jump. If re-allocation is needed during decoding, the decoder will hold up to twice
279+
of `memoryLimit` for a short amount of time.
280+
281+
During decoding the decoder will throw an error, if the needed pixel memory exceeds `memoryLimit`.
282+
283+
Between multiple images the decoder will not free the pixel memory of the previous image.
284+
This is an optimization to lower allocation and GC pressure of the decoder.
285+
Call `release` after decoding to explicitly free the pixel memory.
286+
287+
Rules of thumb regarding memory:
288+
- set `memoryLimit` to a more realistic value, e.g. 64MB for 4096 x 4096 pixels
289+
- conditionally call `release` after image decoding, e.g. check if `memoryUsage` stays within your expectations
290+
- under memory pressure set `memoryLimit` rather low, always call `release`
291+
292+
293+
### Encoder usage
294+
295+
TODO...
296+
297+
298+
### Package format and browser bundles
299+
300+
The node package comes as CommonJS and can be used as usual.
301+
An ESM package version is planned for a later release.
302+
303+
For easier usage in the browser the package contains several prebuilt bundles under `/dist`:
304+
- decode - color functions, default palettes and decoder
305+
- encode - color functions, default palettes and encoder
306+
- full - full package containing all definitions.
307+
308+
The browser bundles come in UMD and ESM flavors. Note that the UMD bundles export
309+
the symbols under `sixel`.
310+
311+
Some usage examples:
312+
- vanilla usage with UMD version:
313+
```html
314+
<script nomodule src="/path/to/decode.umd.js"></script>
315+
...
316+
<script>
317+
sixel.decodeAsync(some_data)
318+
.then(result => someRawImageAction(result.data32, result.width, result.height));
319+
</script>
320+
```
321+
- ESM example:
322+
```html
323+
<script type="module">
324+
import { decodeAsync } from '/path/to/decode.esm.js';
325+
326+
decodeAsync(some_data)
327+
.then(result => someRawImageAction(result.data32, result.width, result.height));
328+
329+
// or with on-demand importing:
330+
import('/path/to/decode.esm.js')
331+
.then(m => m.decodeAsync(some_data))
332+
.then(result => someRawImageAction(result.data32, result.width, result.height));
333+
</script>
334+
```
335+
- web worker example:
336+
```js
337+
importScripts('/path/to/decode.umd.js');
338+
339+
// in web worker we are free to use the sync variants:
340+
const result = sixel.decode(some_data);
341+
someRawImageAction(result.data32, result.width, result.height);
342+
```
343+
344+
197345
### Status
198-
Currently beta, still more tests to come.
346+
Currently beta, still more tests to come. Also the API might still change.
199347

200348

201349
### References

‎bin/install_emscripten.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
if [ -d emsdk ]; then
4+
exit 0
5+
fi
6+
7+
# pull emscripten on fresh checkout
8+
echo "Fetching emscripten..."
9+
10+
git clone https://github.com/emscripten-core/emsdk.git
11+
cd emsdk
12+
13+
# wasm module is only tested with 2.0.25, install by default
14+
./emsdk install 2.0.25
15+
./emsdk activate 2.0.25
16+
17+
cd ..

‎bin/wrap_wasm.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fs = require('fs');
2+
const LIMITS = require('../wasm/settings.json');
3+
4+
const file = `
5+
export const LIMITS = {
6+
CHUNK_SIZE: ${LIMITS.CHUNK_SIZE},
7+
PALETTE_SIZE: ${LIMITS.PALETTE_SIZE},
8+
MAX_WIDTH: ${LIMITS.MAX_WIDTH},
9+
BYTES: '${fs.readFileSync('wasm/decoder.wasm').toString('base64')}'
10+
};
11+
`;
12+
fs.writeFileSync('src/wasm.ts', file);

‎index.html

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,21 @@
6060
Reencoded with img2sixel:<br>
6161
<canvas id="output2" style="border: 1px solid black"></canvas>
6262
<script src="/dist/bundle.js"></script>
63+
<script src="/dist/decode.umd.js"></script>
64+
6365
<script id="sampsa" type="application/json"></script>
6466
<script>
6567

6668
let drawHandle = null;
6769
let imgS = null;
6870
let imgWasm = null;
6971
let wasmDecoder = null;
70-
sixel.WasmDecoderAsync().then(dec => wasmDecoder = dec);
71-
72-
function decodeWasm(bytes) {
73-
const start = new Date();
74-
const dim = new sixel.DimensionDecoder().decode(bytes);
75-
console.log('L2 attrs:', dim);
76-
if (sixel.canUseWasm(dim.width, dim.height, 4096)) {
77-
wasmDecoder.init(
78-
dim.width,
79-
dim.height,
80-
sixel.toRGBA8888(...hexColorToRGB(document.getElementById('fillColor').value))
81-
);
82-
wasmDecoder.decode(bytes);
83-
}
84-
drawImageL2(wasmDecoder.data32, wasmDecoder.width, wasmDecoder.height);
85-
console.log('L2 conversion:', (new Date()) - start);
86-
}
72+
sixel.DecoderAsync().then(dec => wasmDecoder = dec);
8773

88-
function drawImageL2(data, width, height) {
74+
function drawImageWasm() {
75+
const data = wasmDecoder.data32;
76+
const width = wasmDecoder.width;
77+
const height = wasmDecoder.height;
8978
if (!height || !width) {
9079
return;
9180
}
@@ -172,25 +161,31 @@
172161

173162
// create image
174163
const img = new sixel.SixelDecoder();
164+
wasmDecoder.init(sixel.toRGBA8888(...hexColorToRGB(document.getElementById('fillColor').value)));
175165
imgS = img;
176166

177167
// read in
178168
let start;
179169
if (s === 'wiki') {
180170
start = new Date();
181-
img.decodeString('#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~~~@@vv@@~~@@~~~~@@vv@@~~@@~~$#2??}}GG}}??}}????}}GG}}??}}????}}GG}}??}}??-#1!14!14!14@');
171+
img.decodeString('#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~~~@@vv@@~~@@~~~~@@vv@@~~@@~~$#2??}}GG}}??}}????}}GG}}??}}????}}GG}}??}}??$-#1!14!14!14A');
172+
wasmDecoder.decodeString('#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~~~@@vv@@~~@@~~~~@@vv@@~~@@~~$#2??}}GG}}??}}????}}GG}}??}}????}}GG}}??}}??$-#1!42A');
182173
} else {
183174
const response = await fetch('/testfiles/' + s);
184175
const bytes = new Uint8Array(await response.arrayBuffer());
185-
decodeWasm(bytes);
176+
//decodeWasm(bytes);
186177
start = new Date();
187178
if (document.getElementById('slow').checked) {
188-
let localHandle = drawHandle = setInterval(() => drawImage(img), 100);
179+
let localHandle = drawHandle = setInterval(() => {
180+
drawImage(img);
181+
drawImageWasm();
182+
}, 100);
189183
let i = 0;
190184
const endTens = bytes.length - (bytes.length % 10);
191185
while (i < endTens) {
192186
if (drawHandle !== localHandle) return;
193187
img.decode(bytes, i, i + 10);
188+
wasmDecoder.decode(bytes, i, i + 10);
194189
document.getElementById('stats').innerText = 'width: ' + img.width + '\nheight: ' + img.height;
195190
document.getElementById('swidth').value = img.width;
196191
document.getElementById('sheight').value = img.height;
@@ -199,10 +194,33 @@
199194
}
200195
if (bytes.length % 10) {
201196
img.decode(bytes, endTens, bytes.length);
197+
wasmDecoder.decode(bytes, endTens, bytes.length);
198+
//decodeWasm(bytes.subarray(endTens, endTens + bytes.length));
202199
}
203200
clearInterval(drawHandle);
204201
} else {
205202
img.decode(bytes);
203+
decodeWasm(bytes);
204+
wasmDecoder.decode(bytes);
205+
206+
//import('/dist/decode.es6.js').then(m => m.sixelDecodeAsync(bytes)).then(r => {
207+
////sixel_new.sixelDecodeAsync(bytes).then(r => {
208+
// console.log(r);
209+
// const data = r.data32;
210+
// const width = r.width;
211+
// const height = r.height;
212+
// if (!height || !width) {
213+
// return;
214+
// }
215+
// const canvas = document.getElementById('output_wasm');
216+
// const ctx = canvas.getContext('2d');
217+
// // resize canvas to show full image
218+
// canvas.width = width;
219+
// canvas.height = height;
220+
// const target = new ImageData(width, height);
221+
// new Uint32Array(target.data.buffer).set(data);
222+
// ctx.putImageData(target, 0, 0);
223+
//});
206224
}
207225
}
208226
document.getElementById('stats').innerText = 'width: ' + img.width + '\nheight: ' + img.height;
@@ -215,6 +233,7 @@
215233
// output
216234
start = new Date();
217235
drawImage(img);
236+
//drawImageWasm();
218237
console.log('output to canvas time', (new Date()) - start);
219238
}
220239

‎package.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
"prepublish": "npm run tsc",
1313
"coverage": "nyc --reporter=lcov --reporter=text --reporter=html npm test",
1414
"benchmark": "xterm-benchmark $*",
15-
"build-wasm": "bash -c wasm/build.sh"
15+
"build-wasm": "bin/install_emscripten.sh && cd wasm && ./build.sh && cd .. && node bin/wrap_wasm.js",
16+
"bundle": "tsc --project tsconfig.esm.json && webpack",
17+
"clean": "rm -rf lib lib-esm dist src/wasm.ts wasm/decoder.wasm wasm/settings.json",
18+
"build-all": "npm run build-wasm && npm run tsc && npm run bundle"
1619
},
1720
"keywords": [
1821
"sixel",
@@ -25,23 +28,23 @@
2528
},
2629
"author": "Joerg Breitbart <j.breitbart@netzkolchose.de>",
2730
"license": "MIT",
28-
"dependencies": {},
2931
"devDependencies": {
3032
"@istanbuljs/nyc-config-typescript": "^1.0.1",
31-
"@types/mocha": "^8.2.2",
32-
"@types/node": "^14.17.3",
33+
"@types/mocha": "^9.0.0",
34+
"@types/node": "^14.17.12",
3335
"canvas": "^2.8.0",
34-
"http-server": "^0.12.3",
35-
"mocha": "^9.0.1",
36+
"http-server": "^13.0.1",
37+
"mocha": "^9.1.0",
3638
"node-ansiparser": "^2.2.0",
3739
"nyc": "^15.1.0",
3840
"open": "^8.2.1",
3941
"rgbquant": "^1.1.2",
42+
"source-map-loader": "^3.0.0",
4043
"source-map-support": "^0.5.19",
41-
"ts-loader": "^9.2.3",
42-
"ts-node": "^10.0.0",
44+
"ts-loader": "^9.2.5",
45+
"ts-node": "^10.2.1",
4346
"tslint": "^6.1.3",
44-
"typescript": "^4.3.4",
47+
"typescript": "^4.4.2",
4548
"webpack": "^5.40.0",
4649
"webpack-cli": "^4.7.2",
4750
"xterm-benchmark": "^0.2.1"

0 commit comments

Comments
 (0)