Skip to content

Commit 7608bb4

Browse files
committedOct 13, 2019
first unit tests:
- test native color conversion - decode parser transition tests - sanitze color handling: clamp to 100%, always convert to positive ints - memoryUsage property (a way to determine incoming sxiel stream going overboard and to restrict decoding to certain memory limits)
1 parent cab4023 commit 7608bb4

File tree

3 files changed

+271
-11
lines changed

3 files changed

+271
-11
lines changed
 

‎README.md

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ The library provides a class `SixelImage` with the following properties:
2626
Number respresenting the background fill color. A value of 0 will leave background pixels untouched.
2727
The number depends on endianess of the architecture, create it with `toRGBA8888(r, g, b, a)`.
2828

29+
- `memoryUsage: number`
30+
Get current memory usage of the image data in bytes. Can be used to restrict image handling if memory is limited.
31+
Note: This only accounts the image pixel data storage, the real value will be slightly higher due to some JS object overhead.
32+
2933
- `write(data: UintTypedArray, start: number = 0, end: number = data.length): void`
3034
Decodes SIXEL bytes and updates the image data. This is done as a stream,
3135
therefore it is possible to grab partly transmitted images (see "Simulate slow chunks" in browser example).

‎src/index.test.ts

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { assert } from 'chai';
2+
import { SixelImage, fromRGBA8888, toRGBA8888 } from './index';
3+
4+
const BIG_ENDIAN = new Uint8Array(new Uint32Array([0xFF000000]).buffer)[0] === 0xFF;
5+
6+
describe('RGBA8888 native colors', () => {
7+
describe('toRGBA888', () => {
8+
it('conversions', () => {
9+
assert.equal(toRGBA8888(0, 0, 0, 0), 0);
10+
assert.equal(toRGBA8888(0, 0, 0, 255), BIG_ENDIAN ? 0x000000FF : 0xFF000000);
11+
assert.equal(toRGBA8888(0, 0, 255, 0), BIG_ENDIAN ? 0x0000FF00 : 0x00FF0000);
12+
assert.equal(toRGBA8888(0, 255, 0, 0), BIG_ENDIAN ? 0x00FF0000 : 0x0000FF00);
13+
assert.equal(toRGBA8888(255, 0, 0, 0), BIG_ENDIAN ? 0xFF000000 : 0x000000FF);
14+
});
15+
it('alpha defaults to 255', () => {
16+
assert.equal(toRGBA8888(0, 0, 0), toRGBA8888(0, 0, 0, 255));
17+
assert.equal(toRGBA8888(0, 0, 255), toRGBA8888(0, 0, 255, 255));
18+
assert.equal(toRGBA8888(0, 255, 0), toRGBA8888(0, 255, 0, 255));
19+
assert.equal(toRGBA8888(255, 0, 0), toRGBA8888(255, 0, 0, 255));
20+
assert.equal(toRGBA8888(0, 255, 255), toRGBA8888(0, 255, 255, 255));
21+
assert.equal(toRGBA8888(255, 0, 255), toRGBA8888(255, 0, 255, 255));
22+
assert.equal(toRGBA8888(255, 255, 0), toRGBA8888(255, 255, 0, 255));
23+
assert.equal(toRGBA8888(255, 255, 255), toRGBA8888(255, 255, 255, 255));
24+
});
25+
it('should only return unsigned', () => {
26+
// test only for r and a here (g/b dont add to significant bit)
27+
for (let r = 0; r <= 0xFF; ++r) {
28+
for (let a = 0; a <= 0xFF; ++a) {
29+
const color = toRGBA8888(r, 0, 0, a);
30+
assert.equal(color >= 0, true);
31+
}
32+
}
33+
});
34+
it('handled signed channel values', () => {
35+
assert.equal(toRGBA8888(-8, -50, -100, -127), toRGBA8888(-8 >>> 0, -50 >>> 0, -100 >>> 0, -127 >>> 0));
36+
});
37+
it('strip channel values to 8 bit (not clamping)', () => {
38+
assert.equal(toRGBA8888(0x1234, 0x5678, 0xabcd, 0xef11), BIG_ENDIAN ? 0x3478cd11 : 0x11cd7834);
39+
});
40+
});
41+
describe('fromRGBA8888', () => {
42+
it('conversions', () => {
43+
assert.deepEqual(fromRGBA8888(0), [0, 0, 0, 0]);
44+
assert.deepEqual(fromRGBA8888(0x000000FF), BIG_ENDIAN ? [0, 0, 0, 255] : [255, 0, 0, 0]);
45+
assert.deepEqual(fromRGBA8888(0x0000FF00), BIG_ENDIAN ? [0, 0, 255, 0] : [0, 255, 0, 0]);
46+
assert.deepEqual(fromRGBA8888(0x00FF0000), BIG_ENDIAN ? [0, 255, 0, 0] : [0, 0, 255, 0]);
47+
assert.deepEqual(fromRGBA8888(0xFF000000), BIG_ENDIAN ? [255, 0, 0, 0] : [0, 0, 0, 255]);
48+
});
49+
it('should only create unsigned channel values', () => {
50+
assert.deepEqual(fromRGBA8888(-1), [255, 255, 255, 255]);
51+
// 2 complement: -0xedcba988 ==> 0x12345678 (newDigit = 15 - digit; result + 1)
52+
assert.deepEqual(fromRGBA8888(-0xedcba988), BIG_ENDIAN ? [0x12, 0x34, 0x56, 0x78] : [0x78, 0x56, 0x34, 0x12]);
53+
});
54+
it('strip values to 32bit', () => {
55+
assert.deepEqual(fromRGBA8888(0x1234567890), BIG_ENDIAN ? [0x12, 0x34, 0x56, 0x78] : [0x90, 0x78, 0x56, 0x34])
56+
});
57+
});
58+
})
59+
60+
describe('SixelImage', () => {
61+
let img: SixelImage;
62+
beforeEach(() => {
63+
img = new SixelImage();
64+
});
65+
describe('empty data', () => {
66+
it('width/height are 0', () => {
67+
assert.equal(img.width, 0);
68+
assert.equal(img.height, 0);
69+
});
70+
it('toSixel methods should not produce any data', () => {
71+
assert.doesNotThrow(() => img.toSixelBytes(c => { throw Error('should not have been called'); }));
72+
assert.equal(img.toSixelString(), '');
73+
});
74+
it('toImageData does not throw or alter target', () => {
75+
const target = new Uint8ClampedArray(256 * 4);
76+
target.fill(10);
77+
assert.doesNotThrow(() => img.toImageData(target, 16, 16));
78+
assert.deepEqual(target, (new Uint8ClampedArray(256 * 4)).fill(10));
79+
});
80+
});
81+
describe('decode parser', () => {
82+
describe('state transitions', () => {
83+
it('DATA -> DATA', () => {
84+
// excluded chars leading to other states
85+
const except = [33, 34, 35];
86+
const input = new Uint8Array(10);
87+
for (let i = 0; i < 256; ++i) {
88+
if (~except.indexOf(i)) continue;
89+
input[0] = i;
90+
img.write(input, 0, 1);
91+
assert.equal((img as any)._currentState, 0); // 0 == DATA
92+
}
93+
});
94+
it('DATA -> COMPRESSION', () => {
95+
const input = new Uint8Array(10);
96+
input[0] = 33;
97+
img.write(input, 0, 1);
98+
assert.equal((img as any)._currentState, 1); // 1 == COMPRESSION
99+
});
100+
it('DATA -> ATTR', () => {
101+
const input = new Uint8Array(10);
102+
input[0] = 34;
103+
img.write(input, 0, 1);
104+
assert.equal((img as any)._currentState, 2); // 2 == ATTR
105+
});
106+
it('DATA -> COLOR', () => {
107+
const input = new Uint8Array(10);
108+
input[0] = 35;
109+
img.write(input, 0, 1);
110+
assert.equal((img as any)._currentState, 3); // 3 == COLOR
111+
});
112+
it('COMPRESSION -> COMPRESSION', () => {
113+
(img as any)._currentState = 1;
114+
const input = new Uint8Array(10);
115+
for (let i = 0; i < 256; ++i) {
116+
if (63 <= i && i <= 126) continue;
117+
input[0] = i;
118+
img.write(input, 0, 1);
119+
assert.equal((img as any)._currentState, 1);
120+
}
121+
});
122+
it('COMPRESSION -> DATA', () => {
123+
(img as any)._currentState = 1;
124+
const input = new Uint8Array(10);
125+
for (let i = 63; i < 127; ++i) {
126+
input[0] = i;
127+
img.write(input, 0, 1);
128+
assert.equal((img as any)._currentState, 0);
129+
(img as any)._currentState = 1;
130+
}
131+
});
132+
it('ATTR -> ATTR', () => {
133+
// excluded chars leading to other states
134+
const except = [33, 35, 36, 45];
135+
const input = new Uint8Array(10);
136+
(img as any)._currentState = 2;
137+
for (let i = 0; i < 256; ++i) {
138+
if (~except.indexOf(i)) continue;
139+
if (63 <= i && i <= 126) continue;
140+
input[0] = i;
141+
img.write(input, 0, 1);
142+
assert.equal((img as any)._currentState, 2);
143+
(img as any)._currentState = 2;
144+
}
145+
});
146+
it('ATTR -> DATA', () => {
147+
(img as any)._currentState = 2;
148+
const input = new Uint8Array(10);
149+
for (let i = 63; i < 127; ++i) {
150+
input[0] = i;
151+
img.write(input, 0, 1);
152+
assert.equal((img as any)._currentState, 0);
153+
(img as any)._currentState = 2;
154+
}
155+
(img as any)._currentState = 2;
156+
input[0] = 36;
157+
img.write(input, 0, 1);
158+
assert.equal((img as any)._currentState, 0);
159+
(img as any)._currentState = 2;
160+
input[0] = 45;
161+
img.write(input, 0, 1);
162+
assert.equal((img as any)._currentState, 0);
163+
});
164+
it('ATTR -> COMPRESSION', () => {
165+
(img as any)._currentState = 2;
166+
const input = new Uint8Array(10);
167+
input[0] = 33;
168+
img.write(input, 0, 1);
169+
assert.equal((img as any)._currentState, 1); // 1 == COMPRESSION
170+
});
171+
it('ATTR -> COLOR', () => {
172+
(img as any)._currentState = 2;
173+
const input = new Uint8Array(10);
174+
input[0] = 35;
175+
img.write(input, 0, 1);
176+
assert.equal((img as any)._currentState, 3); // 3 == COLOR
177+
});
178+
it('COLOR -> COLOR', () => {
179+
// excluded chars leading to other states
180+
const except = [33, 34, 36, 45];
181+
const input = new Uint8Array(10);
182+
(img as any)._currentState = 3;
183+
for (let i = 0; i < 256; ++i) {
184+
if (~except.indexOf(i)) continue;
185+
if (63 <= i && i <= 126) continue;
186+
input[0] = i;
187+
img.write(input, 0, 1);
188+
assert.equal((img as any)._currentState, 3);
189+
(img as any)._currentState = 3;
190+
}
191+
});
192+
it('COLOR -> DATA', () => {
193+
(img as any)._currentState = 3;
194+
const input = new Uint8Array(10);
195+
for (let i = 63; i < 127; ++i) {
196+
input[0] = i;
197+
img.write(input, 0, 1);
198+
assert.equal((img as any)._currentState, 0);
199+
(img as any)._currentState = 3;
200+
}
201+
(img as any)._currentState = 3;
202+
input[0] = 36;
203+
img.write(input, 0, 1);
204+
assert.equal((img as any)._currentState, 0);
205+
(img as any)._currentState = 3;
206+
input[0] = 45;
207+
img.write(input, 0, 1);
208+
assert.equal((img as any)._currentState, 0);
209+
});
210+
it('COLOR -> COMPRESSION', () => {
211+
(img as any)._currentState = 3;
212+
const input = new Uint8Array(10);
213+
input[0] = 33;
214+
img.write(input, 0, 1);
215+
assert.equal((img as any)._currentState, 1); // 1 == COMPRESSION
216+
});
217+
it('COLOR -> ATTR', () => {
218+
(img as any)._currentState = 3;
219+
const input = new Uint8Array(10);
220+
input[0] = 34;
221+
img.write(input, 0, 1);
222+
assert.equal((img as any)._currentState, 2); // 2 == ATTR
223+
});
224+
});
225+
});
226+
});

‎src/index.ts

+41-11
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,14 @@ function hlsToRgb(h: number, l: number, s: number): RGBA8888 {
137137
}
138138

139139
return (BIG_ENDIAN)
140-
? Math.round(r * 255) << 24 | Math.round(g * 255) << 16 | Math.round(b * 255) << 8 | 0xFF // RGBA32
141-
: 0xFF000000 | Math.round(b * 255) << 16 | Math.round(g * 255) << 8 | Math.round(r * 255); // ABGR32
140+
? (Math.round(r * 255) << 24 | Math.round(g * 255) << 16 | Math.round(b * 255) << 8 | 0xFF) >>> 0 // RGBA32
141+
: (0xFF000000 | Math.round(b * 255) << 16 | Math.round(g * 255) << 8 | Math.round(r * 255)) >>> 0; // ABGR32
142142
}
143143

144144
function normalizeRGB(r: number, g: number, b: number): RGBA8888 {
145145
return (BIG_ENDIAN)
146-
? Math.round(r / 100 * 255) << 24 | Math.round(g / 100 * 255) << 16 | Math.round(b / 100 * 255) << 8 | 0xFF // RGBA32
147-
: 0xFF000000 | Math.round(b / 100 * 255) << 16 | Math.round(g / 100 * 255) << 8 | Math.round(r / 100 * 255); // ABGR32
146+
? (Math.round(r / 100 * 255) << 24 | Math.round(g / 100 * 255) << 16 | Math.round(b / 100 * 255) << 8 | 0xFF) >>> 0 // RGBA32
147+
: (0xFF000000 | Math.round(b / 100 * 255) << 16 | Math.round(g / 100 * 255) << 8 | Math.round(r / 100 * 255)) >>> 0; // ABGR32
148148
}
149149

150150
function normalizeHLS(h: number, l: number, s: number): RGBA8888 {
@@ -188,6 +188,13 @@ class SixelBand {
188188
this.data = new Uint32Array(length * 6);
189189
}
190190

191+
/**
192+
* Get current memory usage of the band.
193+
*/
194+
public get memUsage(): number {
195+
return this.data.length * 4;
196+
}
197+
191198
/**
192199
* Add a sixel to the band.
193200
* Called by the parser for any data byte of the sixel stream.
@@ -464,7 +471,7 @@ const SIXEL_TABLE = (() => {
464471
// default transition for all states
465472
for (state in states) {
466473
// Note: ignore never changes state
467-
table.addMany(r(0x00, 0x80), state, SixelAction.IGNORE, state);
474+
table.addMany(r(0x00, 0x100), state, SixelAction.IGNORE, state);
468475
}
469476
// DATA state
470477
table.addMany(r(63, 127), SixelState.DATA, SixelAction.DRAW, SixelState.DATA);
@@ -648,6 +655,16 @@ export class SixelImage {
648655
return this._width || Math.max.apply(null, this._bands.map(el => el.width)) | 0;
649656
}
650657

658+
/**
659+
* Get current memory usage of the image data in bytes.
660+
* Can be used to restrict image handling if memory is limited.
661+
* Note: This only accounts the image pixel data storage, the real value
662+
* will be slightly higher due to some JS object overhead.
663+
*/
664+
public get memUsage(): number {
665+
return this._bands.reduce((accu, cur) => accu + cur.memUsage, 0);
666+
}
667+
651668
/**
652669
* Write SIXEL string data to the image.
653670
*/
@@ -671,8 +688,8 @@ export class SixelImage {
671688
let params = this._params;
672689

673690
for (let i = start; i < end; ++i) {
674-
const code = data[i];
675-
const transition = SIXEL_TABLE.table[currentState << 8 | (code < 0x7F ? code : 0xFF)];
691+
const code = data[i] & 0xFF;
692+
const transition = SIXEL_TABLE.table[currentState << 8 | (code < 0x7F ? code : 0xFF)] | 0;
676693
switch (transition >> 4) {
677694
case SixelAction.DRAW:
678695
dataStart = (~dataStart) ? dataStart : i;
@@ -736,11 +753,19 @@ export class SixelImage {
736753
if (currentState === SixelState.COLOR) {
737754
if (params.length >= 5) {
738755
if (params[1] === 1) {
739-
// HLS color
740-
this._colors[params[0]] = color = normalizeHLS(params[2], params[3], params[4]);
756+
// HLS color, angle as mod 260, LS in % clamped to 100
757+
this._colors[params[0]] = color = normalizeHLS(
758+
params[2] % 360,
759+
Math.min(params[3], 100),
760+
Math.min(params[4], 100)
761+
);
741762
} else if (params[1] === 2) {
742-
// RGB color
743-
this._colors[params[0]] = color = normalizeRGB(params[2], params[3], params[4]);
763+
// RGB color in %, clamped to 100
764+
this._colors[params[0]] = color = normalizeRGB(
765+
Math.min(params[2], 100),
766+
Math.min(params[3], 100),
767+
Math.min(params[4], 100)
768+
);
744769
}
745770
} else if (params.length === 1) {
746771
color = this._colors[params[0]] || this._colors[0];
@@ -854,6 +879,11 @@ export class SixelImage {
854879
* @param cb callback to process a single SIXEL chunk (borrowed)
855880
*/
856881
public toSixelBytes(cb: (chunk: Uint8Array) => void): void {
882+
// exit early if we have no image data
883+
if (!this.width || !this.height) {
884+
return;
885+
}
886+
857887
// prepare chunk buffer
858888
if (!this._chunk || this._chunk.length < this.width + 100) {
859889
this._chunk = new Uint8Array(this.width + 100);

0 commit comments

Comments
 (0)
Please sign in to comment.