Skip to content

Commit 4ffec76

Browse files
committed
calculate real height in M1 mode
1 parent 1b9f49f commit 4ffec76

8 files changed

+80
-11
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ It exposes the following properties:
5858
Reports the current width of the current image. For `truncate=true` this may report the raster width, if a valid raster attribute was found. Otherwise reports rightmost band cursor advance seen so far.
5959

6060
- `height: number`
61-
Reports the current height of the current image. Note that for `trancate=true` this may report the raster height, if a valid raster attribute was found. Otherwise reports the height in multiple of 6 pixels (seen sixel bands).
61+
Reports the current height of the current image. Note that for `trancate=true` this may report the raster height, if a valid raster attribute was found. Otherwise reports the lowermost pixel position touched by a sixel.
6262

6363
- `memoryUsage: number`
6464
Reports the current memory usage of the decoder for wasm module memory and allocated pixel buffer.

index.html

+19-1
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@
3535
<br><br>
3636
<label><input type="checkbox" name="slow" id="slow" value="value"> Simulate slow chunks</label>
3737
<br>
38+
<label><input type="checkbox" name="m1" id="m1" value="value"> Force M1 Mode (truncate: false)</label>
39+
<br>
3840
<label><input type="color" name="fillColor" id="fillColor" value="#000000"> background color</label>
3941
<br><br>
4042
<span id="stats"></span>
43+
<br>
44+
<span id="decodetime"></span>
4145
<br><br>
4246
<canvas id="output_wasm" style="border: 1px solid black"></canvas>
4347
<script src="/dist/full.umd.js"></script>
@@ -75,7 +79,16 @@
7579
clearInterval(drawHandle);
7680

7781
// initialize decoder
78-
decoder.init(sixel.toRGBA8888(...hexColorToRGB(document.getElementById('fillColor').value)));
82+
decoder.init(
83+
// fillColor - BG color
84+
sixel.toRGBA8888(...hexColorToRGB(document.getElementById('fillColor').value)),
85+
// palette (null: dont change, undefined: pull default)
86+
undefined,
87+
// paletteLimit - max usable palette slots
88+
256,
89+
// truncate - whether to cut at raster dimensions (false - always M1 mode, true - maybe M2)
90+
!document.getElementById('m1').checked
91+
);
7992

8093
// read in
8194
if (s === 'wiki') {
@@ -84,6 +97,9 @@
8497
const response = await fetch('/testfiles/' + s);
8598
const bytes = new Uint8Array(await response.arrayBuffer());
8699
if (document.getElementById('slow').checked) {
100+
// inspect slow transmission image construction
101+
// (dont copy this, its just a wild hack w'o proper async state handling)
102+
document.getElementById('decodetime').innerText = '';
87103
let localHandle = drawHandle = setInterval(() => drawImageWasm(), 100);
88104
let i = 0;
89105
const endTens = bytes.length - (bytes.length % 10);
@@ -99,7 +115,9 @@
99115
}
100116
clearInterval(drawHandle);
101117
} else {
118+
const start = Date.now();
102119
decoder.decode(bytes);
120+
document.getElementById('decodetime').innerText = 'decoding time: ' + (Date.now() - start) + 'ms';
103121
}
104122
}
105123
document.getElementById('stats').innerText = 'width: ' + decoder.width + '\nheight: ' + decoder.height;

src/Decoder.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,35 @@ describe('Decoder', () => {
986986
dec.init(0, null, 256);
987987
assert.strictEqual(dec.data8.length, 0);
988988
});
989+
it.only('M1 reports and uses correct with/height', () => {
990+
const dec = new Decoder();
991+
// no sixels at all
992+
dec.decodeString('#0;');
993+
assert.strictEqual(dec.width, 0);
994+
assert.strictEqual(dec.height, 0);
995+
assert.strictEqual(dec.data32.length, 0);
996+
997+
// helper to generate arbitrary diagonale
998+
function diag(n: number) {
999+
const d6 = '@ACGO_';
1000+
let final = '';
1001+
for (let i = 0; i < Math.floor(n / 6); ++i) {
1002+
final += d6;
1003+
final += '$-!' + ((i+1)*6) + '?';
1004+
}
1005+
final += d6.slice(0, n % 6)
1006+
return final;
1007+
}
1008+
1009+
// draw diagonale as 1x1, 2x2, 3x3, ... 19x19
1010+
for (let h = 1; h < 19; ++h) {
1011+
dec.init(0, null, 256);
1012+
dec.decodeString(diag(h));
1013+
assert.strictEqual(dec.width, h);
1014+
assert.strictEqual(dec.height, h);
1015+
assert.strictEqual(dec.data32.length, h*h);
1016+
}
1017+
});
9891018
});
9901019
describe('release', () => {
9911020
const data = fs.readFileSync('./testfiles/test1_clean.sixel');

src/Decoder.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,13 @@ export class Decoder {
253253
/**
254254
* Height of the image data.
255255
* Returns the rasterHeight in level2/truncating mode,
256-
* otherwise 6 * seen bands.
256+
* otherwise height touched by sixels.
257257
*/
258258
public get height(): number {
259259
return this._mode !== ParseMode.M1
260260
? this._height
261261
: this._wasm.current_width()
262-
? this._bandWidths.length * 6 + 6
262+
? this._bandWidths.length * 6 + this._wasm.current_height()
263263
: this._bandWidths.length * 6;
264264
}
265265

@@ -367,7 +367,7 @@ export class Decoder {
367367
}
368368

369369
// get width of pending band to peek into left-over data
370-
let currentWidth = this._wasm.current_width();
370+
const currentWidth = this._wasm.current_width();
371371

372372
if (this._mode === ParseMode.M2) {
373373
let remaining = this.height - this._currentHeight;
@@ -407,7 +407,8 @@ export class Decoder {
407407
}
408408
}
409409

410-
// worst case: copy re-aligned pixels if we have bands with different width
410+
// worst case: re-align pixels if we have bands with different width
411+
// This is somewhat allocation intensive, any way to do that in-place, and just once?
411412
const final = new Uint32Array(this.width * this.height);
412413
final.fill(this._fillColor);
413414
let finalOffset = 0;
@@ -419,9 +420,12 @@ export class Decoder {
419420
finalOffset += this.width;
420421
}
421422
}
423+
// also handle left-over pixels of the current band
422424
if (currentWidth) {
423425
const adv = this._PIXEL_OFFSET;
424-
for (let i = 0; i < 6; ++i) {
426+
// other than finished bands, this runs only up to currentHeight
427+
const currentHeight = this._wasm.current_height();
428+
for (let i = 0; i < currentHeight; ++i) {
425429
final.set(this._pSrc.subarray(adv * i, adv * i + currentWidth), finalOffset + this.width * i);
426430
}
427431
}

src/Types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export interface IWasmDecoderExports extends Record<string, WebAssembly.ExportVa
144144
init(sixelColor: number, fillColor: number, paletteLimit: number, truncate: number): void;
145145
decode(start: number, end: number): void;
146146
current_width(): number;
147+
current_height(): number;
147148
}
148149

149150
// wasm decoder

wasm/README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ Exported symbols:
9090
Return the cursor advance of the current band in M1 mode, or width in M2 mode.
9191
This is needed to properly construct the full image at the end of decoding,
9292
in case the data did not finish with LF.
93+
- `int current_width()`
94+
M1 mode only - return the current lowermost pixel position touched by a sixel
95+
of the current band. This is needed to properly construct the full image with
96+
proper height for the current band, in case the data did not finish with LF.
9397

9498
Needed callbacks:
9599
- `int mode_parsed(int mode)`
@@ -121,10 +125,10 @@ Needed callbacks:
121125
raster dimensions on decoder side is not spec-conform, it is the expected data format created by
122126
a spec-conform encoder.
123127
- If `truncate` is not set, the width will be derived from cursor advance to the right, clamped
124-
to `MAX_WIDTH-4`. In this mode, the height is always reported in terms of multiple of 6
125-
(pixel height of a sixel band). Furthermore in this mode the height is not limited by any means,
126-
thus decoding may run forever. Use some sort of accounting during `handle_band` to spot
127-
malformed data or excessive memory usage, especially when dealing with data streams.
128+
to `MAX_WIDTH-4`. In this mode, `current_height` is reported as lowermost pixel position touched by sixels.
129+
Furthermore in this mode the height is not limited by any means, thus decoding may run forever.
130+
Use some sort of accounting during `handle_band` to spot malformed data or excessive memory usage,
131+
especially when dealing with data streams.
128132
- Palette colors are applied immediately to sixels (printer mode), there is no terminal-like indexed mode.
129133
While this is in line with the spec, it does not allow to mimick the palette behavior of older terminals
130134
(e.g. palette animations are not possible).

wasm/build.sh

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ emcc -O3 \
6666
"_init",
6767
"_decode",
6868
"_current_width",
69+
"_current_height",
6970
"_get_state_address",
7071
"_get_chunk_address",
7172
"_get_p0_address",

wasm/decoder.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ static struct {
5555
int abort;
5656
int cleared_width;
5757
int real_width;
58+
int band_height;
5859
int state;
5960
int color;
6061
int cursor;
@@ -83,6 +84,7 @@ extern "C" {
8384
void init(int sixel_color, int fill_color, unsigned int palette_length, int truncate);
8485
void decode(int start, int end);
8586
int current_width();
87+
int current_height();
8688

8789
// imported
8890
int handle_band(int width);
@@ -209,6 +211,7 @@ static inline void clear_next() {
209211
// Clear pixel buffers for next line processing (m1). Hardcoded to 128px chunk.
210212
static inline void reset_line_m1() {
211213
ps.real_width = 4;
214+
ps.band_height = 0;
212215

213216
// fill 128 pixels in p0 as copy source
214217
long long *blueprint = (long long *) &ps.p0[4];
@@ -289,6 +292,7 @@ void decode_m1(int start, int end) {
289292
int k = ps.params[0] ? ps.params[0] : 1;
290293
while (cur + k >= ps.cleared_width && ps.cleared_width < MAX_WIDTH) clear_next();
291294
put(code - 63, color, k, cur);
295+
ps.band_height |= code - 63;
292296
cur += k;
293297
code = *c++ & 0x7F;
294298
} else {
@@ -299,6 +303,7 @@ void decode_m1(int start, int end) {
299303
while (unsigned(code - 63) < 64) {
300304
if (cur >= ps.cleared_width && ps.cleared_width < MAX_WIDTH) clear_next(); // FIXME: MAX_WIDTH is exp here
301305
put_single(code - 63, color, cur++);
306+
ps.band_height |= code - 63;
302307
code = *c++ & 0x7F;
303308
};
304309
}
@@ -505,6 +510,7 @@ void init(int sixel_color, int fill_color, unsigned int palette_length, int trun
505510
ps.r_height = 0;
506511
ps.width = 0;
507512
ps.height = 0;
513+
ps.band_height = 0;
508514
ps.abort = 0;
509515
}
510516

@@ -526,3 +532,9 @@ int current_width() {
526532
}
527533
return 0;
528534
}
535+
536+
// Height of the current band (M1 only).
537+
int current_height() {
538+
int x = ps.band_height;
539+
return x & 32 ? 6 : x & 16 ? 5 : x & 8 ? 4 : x & 4 ? 3 : x & 2 ? 2 : x & 1 ? 1 : 0;
540+
}

0 commit comments

Comments
 (0)