@xterm/addon-image 0.10.0-beta.28 → 0.10.0-beta.281
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -42
- package/lib/addon-image.js +1 -1
- package/lib/addon-image.js.map +1 -1
- package/lib/addon-image.mjs +1 -22
- package/lib/addon-image.mjs.map +4 -4
- package/package.json +5 -5
- package/src/IIPHandler.ts +109 -41
- package/src/IIPHeaderParser.ts +89 -13
- package/src/IIPImageStorage.ts +28 -0
- package/src/IIPMetrics.ts +9 -1
- package/src/ImageAddon.ts +74 -13
- package/src/ImageRenderer.ts +99 -53
- package/src/ImageStorage.ts +138 -65
- package/src/SixelHandler.ts +4 -4
- package/src/SixelImageStorage.ts +52 -0
- package/src/Types.ts +18 -3
- package/src/kitty/KittyGraphicsHandler.ts +819 -0
- package/src/kitty/KittyGraphicsTypes.ts +195 -0
- package/src/kitty/KittyImageStorage.ts +155 -0
- package/typings/addon-image.d.ts +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xterm/addon-image",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.281",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "The xterm.js authors",
|
|
6
6
|
"url": "https://xtermjs.org/"
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
"xterm.js"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"prepackage": "../../node_modules/.bin/
|
|
21
|
+
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
|
22
22
|
"package": "../../node_modules/.bin/webpack",
|
|
23
23
|
"prepublishOnly": "npm run package",
|
|
24
24
|
"start": "node ../../demo/start"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"sixel": "^0.16.0",
|
|
28
|
-
"xterm-wasm-parts": "^0.1
|
|
28
|
+
"xterm-wasm-parts": "^0.4.1"
|
|
29
29
|
},
|
|
30
|
-
"commit": "
|
|
30
|
+
"commit": "2fe3fd13a164f956d0c6a2365fa89feaf4366074",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
32
|
+
"@xterm/xterm": "^6.1.0-beta.281"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/IIPHandler.ts
CHANGED
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
|
6
6
|
import { ImageRenderer } from './ImageRenderer';
|
|
7
|
-
import {
|
|
7
|
+
import { IIPImageStorage } from './IIPImageStorage';
|
|
8
|
+
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
8
9
|
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
|
9
|
-
import
|
|
10
|
+
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
|
11
|
+
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
|
10
12
|
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// Local const enum mirror - esbuild can't inline const enums from external packages
|
|
15
|
+
const enum DecoderConst {
|
|
16
|
+
// Limit held memory in base64 decoder (encoded bytes).
|
|
17
|
+
KEEP_DATA = 4194304,
|
|
18
|
+
// Initial buffer allocation for the decoder.
|
|
19
|
+
INITIAL_DATA = 1048576,
|
|
20
|
+
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
|
21
|
+
OK = 0
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
// default IIP header values
|
|
20
25
|
const DEFAULT_HEADER: IHeaderFields = {
|
|
26
|
+
type: SequenceType.INVALID,
|
|
21
27
|
name: 'Unnamed file',
|
|
22
28
|
size: 0,
|
|
23
29
|
width: 'auto',
|
|
@@ -31,21 +37,32 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
31
37
|
private _aborted = false;
|
|
32
38
|
private _hp = new HeaderParser();
|
|
33
39
|
private _header: IHeaderFields = DEFAULT_HEADER;
|
|
34
|
-
private _dec
|
|
40
|
+
private _dec: Base64Decoder;
|
|
41
|
+
private _qoiDec: QoiDecoder;
|
|
35
42
|
private _metrics = UNSUPPORTED_TYPE;
|
|
43
|
+
private _isMultipart = false;
|
|
44
|
+
private _abortMulti = false;
|
|
36
45
|
|
|
37
46
|
constructor(
|
|
38
47
|
private readonly _opts: IImageAddonOptions,
|
|
39
48
|
private readonly _renderer: ImageRenderer,
|
|
40
|
-
private readonly _storage:
|
|
49
|
+
private readonly _storage: IIPImageStorage,
|
|
41
50
|
private readonly _coreTerminal: ITerminalExt
|
|
42
|
-
) {
|
|
51
|
+
) {
|
|
52
|
+
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
|
53
|
+
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
|
54
|
+
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
|
|
55
|
+
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
|
|
56
|
+
}
|
|
43
57
|
|
|
44
|
-
public reset(): void {
|
|
58
|
+
public reset(): void {
|
|
59
|
+
this._hp.reset();
|
|
60
|
+
this._dec.release();
|
|
61
|
+
this._qoiDec.release();
|
|
62
|
+
}
|
|
45
63
|
|
|
46
64
|
public start(): void {
|
|
47
65
|
this._aborted = false;
|
|
48
|
-
this._header = DEFAULT_HEADER;
|
|
49
66
|
this._metrics = UNSUPPORTED_TYPE;
|
|
50
67
|
this._hp.reset();
|
|
51
68
|
}
|
|
@@ -54,7 +71,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
54
71
|
if (this._aborted) return;
|
|
55
72
|
|
|
56
73
|
if (this._hp.state === HeaderState.END) {
|
|
57
|
-
if (this._dec.put(data
|
|
74
|
+
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
|
|
58
75
|
this._dec.release();
|
|
59
76
|
this._aborted = true;
|
|
60
77
|
}
|
|
@@ -65,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
65
82
|
return;
|
|
66
83
|
}
|
|
67
84
|
if (dataPos > 0) {
|
|
68
|
-
|
|
69
|
-
if (
|
|
85
|
+
const seqType = this._hp.fields.type;
|
|
86
|
+
if (seqType === SequenceType.FILE) {
|
|
87
|
+
if (this._isMultipart) {
|
|
88
|
+
this._isMultipart = false;
|
|
89
|
+
this._abortMulti = false;
|
|
90
|
+
this._dec.release();
|
|
91
|
+
}
|
|
92
|
+
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
|
93
|
+
if (!this._header.inline) {
|
|
94
|
+
this._aborted = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this._dec.init();
|
|
98
|
+
} else if (this._abortMulti) {
|
|
70
99
|
this._aborted = true;
|
|
71
100
|
return;
|
|
72
101
|
}
|
|
73
|
-
this._dec.
|
|
74
|
-
if (this._dec.put(data, dataPos, end)) {
|
|
102
|
+
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
|
75
103
|
this._dec.release();
|
|
76
104
|
this._aborted = true;
|
|
105
|
+
if (this._isMultipart) this._abortMulti = true;
|
|
77
106
|
}
|
|
78
107
|
}
|
|
79
108
|
}
|
|
@@ -82,11 +111,49 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
82
111
|
public end(success: boolean): boolean | Promise<boolean> {
|
|
83
112
|
if (this._aborted) return true;
|
|
84
113
|
|
|
114
|
+
if (this._hp.state !== HeaderState.END) {
|
|
115
|
+
if (this._hp.end()) return true;
|
|
116
|
+
}
|
|
117
|
+
const seqType = this._hp.fields.type;
|
|
118
|
+
|
|
119
|
+
if (seqType === SequenceType.FILEPART) return true;
|
|
120
|
+
|
|
121
|
+
if (seqType === SequenceType.REPORTCELLSIZE) {
|
|
122
|
+
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
|
123
|
+
let width = CELL_SIZE_DEFAULT.width;
|
|
124
|
+
let height = CELL_SIZE_DEFAULT.height;
|
|
125
|
+
if (this._renderer.dimensions) {
|
|
126
|
+
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
|
127
|
+
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
|
128
|
+
}
|
|
129
|
+
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
|
130
|
+
const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
|
131
|
+
this._coreTerminal.input(report, false);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (seqType === SequenceType.MULTIPARTFILE) {
|
|
136
|
+
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
|
137
|
+
this._isMultipart = true;
|
|
138
|
+
this._abortMulti = false;
|
|
139
|
+
this._dec.release();
|
|
140
|
+
this._dec.init();
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (seqType === SequenceType.FILEEND) {
|
|
145
|
+
if (!this._isMultipart) return true;
|
|
146
|
+
this._isMultipart = false;
|
|
147
|
+
if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
|
151
|
+
|
|
85
152
|
let w = 0;
|
|
86
153
|
let h = 0;
|
|
87
154
|
|
|
88
155
|
// early exit condition chain
|
|
89
|
-
let cond: number | boolean
|
|
156
|
+
let cond: number | boolean;
|
|
90
157
|
if (cond = success) {
|
|
91
158
|
if (cond = !this._dec.end()) {
|
|
92
159
|
this._metrics = imageType(this._dec.data8);
|
|
@@ -105,26 +172,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
105
172
|
return true;
|
|
106
173
|
}
|
|
107
174
|
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
175
|
+
let blob: Blob | ImageData;
|
|
176
|
+
if (this._metrics.mime === 'image/qoi') {
|
|
177
|
+
const data = this._qoiDec.decode(this._dec.data8);
|
|
178
|
+
blob = new ImageData(
|
|
179
|
+
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
|
180
|
+
this._qoiDec.width,
|
|
181
|
+
this._qoiDec.height
|
|
182
|
+
);
|
|
183
|
+
this._qoiDec.release();
|
|
184
|
+
if (w === this._qoiDec.width && h === this._qoiDec.height) {
|
|
185
|
+
// use fast-path if we don't need to rescale
|
|
186
|
+
this._dec.release();
|
|
187
|
+
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
|
|
188
|
+
canvas.getContext('2d')?.putImageData(blob, 0, 0);
|
|
189
|
+
this._storage.addImage(canvas);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
|
127
194
|
}
|
|
195
|
+
this._dec.release();
|
|
128
196
|
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
|
129
197
|
.then(bm => {
|
|
130
198
|
this._storage.addImage(bm);
|
|
@@ -154,8 +222,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
154
222
|
|
|
155
223
|
private _dim(s: string, total: number, cdim: number): number {
|
|
156
224
|
if (s === 'auto') return 0;
|
|
157
|
-
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
|
|
158
|
-
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
|
|
159
|
-
return parseInt(s) * cdim;
|
|
225
|
+
if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
|
|
226
|
+
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
|
|
227
|
+
return parseInt(s, 10) * cdim;
|
|
160
228
|
}
|
|
161
229
|
}
|
package/src/IIPHeaderParser.ts
CHANGED
|
@@ -6,8 +6,27 @@
|
|
|
6
6
|
// eslint-disable-next-line
|
|
7
7
|
declare const Buffer: any;
|
|
8
8
|
|
|
9
|
+
export const enum HeaderState {
|
|
10
|
+
START = 0,
|
|
11
|
+
ABORT = 1,
|
|
12
|
+
KEY = 2,
|
|
13
|
+
VALUE = 3,
|
|
14
|
+
END = 4
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const enum SequenceType {
|
|
18
|
+
INVALID = 0,
|
|
19
|
+
FILE = 1,
|
|
20
|
+
MULTIPARTFILE = 2,
|
|
21
|
+
FILEPART = 3,
|
|
22
|
+
FILEEND = 4,
|
|
23
|
+
REPORTCELLSIZE = 5
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
export interface IHeaderFields {
|
|
27
|
+
[key: string]: number | string | Uint32Array | null | undefined;
|
|
28
|
+
// sequence type
|
|
29
|
+
type: SequenceType;
|
|
11
30
|
// base-64 encoded filename. Defaults to "Unnamed file".
|
|
12
31
|
name: string;
|
|
13
32
|
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
|
@@ -28,14 +47,6 @@ export interface IHeaderFields {
|
|
|
28
47
|
inline?: number;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
export const enum HeaderState {
|
|
32
|
-
START = 0,
|
|
33
|
-
ABORT = 1,
|
|
34
|
-
KEY = 2,
|
|
35
|
-
VALUE = 3,
|
|
36
|
-
END = 4
|
|
37
|
-
}
|
|
38
|
-
|
|
39
50
|
// field value decoders
|
|
40
51
|
|
|
41
52
|
// ASCII bytes to string
|
|
@@ -81,7 +92,7 @@ function toName(data: Uint32Array): string {
|
|
|
81
92
|
return new TextDecoder().decode(b);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
const DECODERS: {[key: string]: (v: Uint32Array) =>
|
|
95
|
+
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
|
85
96
|
inline: toInt,
|
|
86
97
|
size: toInt,
|
|
87
98
|
name: toName,
|
|
@@ -91,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
|
|
91
102
|
};
|
|
92
103
|
|
|
93
104
|
|
|
105
|
+
// sequence type markers
|
|
106
|
+
// File
|
|
94
107
|
const FILE_MARKER = [70, 105, 108, 101];
|
|
108
|
+
// MultipartFile
|
|
109
|
+
const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
|
|
110
|
+
// FilePart
|
|
111
|
+
const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
|
|
112
|
+
// FileEnd
|
|
113
|
+
const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
|
|
114
|
+
// ReportCellSize
|
|
115
|
+
const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
|
|
116
|
+
|
|
117
|
+
// max allowed chars for sequence header
|
|
95
118
|
const MAX_FIELDCHARS = 1024;
|
|
96
119
|
|
|
97
120
|
|
|
@@ -100,7 +123,7 @@ export class HeaderParser {
|
|
|
100
123
|
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
|
101
124
|
private _position = 0;
|
|
102
125
|
private _key = '';
|
|
103
|
-
public fields: {[key: string]:
|
|
126
|
+
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
|
104
127
|
|
|
105
128
|
public reset(): void {
|
|
106
129
|
this._buffer.fill(0);
|
|
@@ -110,12 +133,43 @@ export class HeaderParser {
|
|
|
110
133
|
this._key = '';
|
|
111
134
|
}
|
|
112
135
|
|
|
136
|
+
public end(): number {
|
|
137
|
+
if (this.state === HeaderState.START) {
|
|
138
|
+
if (this._position === FILEEND_MARKER.length) {
|
|
139
|
+
for (let k = 0; k < FILEEND_MARKER.length; ++k) {
|
|
140
|
+
if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
|
|
141
|
+
}
|
|
142
|
+
this.fields['type'] = SequenceType.FILEEND;
|
|
143
|
+
this.state = HeaderState.END;
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
if (this._position === REPORTCELLSIZE_MARKER.length) {
|
|
147
|
+
for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
|
|
148
|
+
if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
|
|
149
|
+
}
|
|
150
|
+
this.fields['type'] = SequenceType.REPORTCELLSIZE;
|
|
151
|
+
this.state = HeaderState.END;
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
return this._a();
|
|
155
|
+
}
|
|
156
|
+
if (this.state === HeaderState.END) return 0;
|
|
157
|
+
if (this.state === HeaderState.VALUE
|
|
158
|
+
&& this.fields.type === SequenceType.MULTIPARTFILE
|
|
159
|
+
) {
|
|
160
|
+
if (!this._storeValue(this._position)) return this._a();
|
|
161
|
+
this.state = HeaderState.END;
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
return this._a();
|
|
165
|
+
}
|
|
166
|
+
|
|
113
167
|
public parse(data: Uint32Array, start: number, end: number): number {
|
|
114
168
|
let state = this.state;
|
|
115
169
|
let pos = this._position;
|
|
116
170
|
const buffer = this._buffer;
|
|
117
171
|
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
|
|
118
|
-
if (state === HeaderState.START && pos >
|
|
172
|
+
if (state === HeaderState.START && pos > 14) return -1;
|
|
119
173
|
for (let i = start; i < end; ++i) {
|
|
120
174
|
const c = data[i];
|
|
121
175
|
switch (c) {
|
|
@@ -126,8 +180,29 @@ export class HeaderParser {
|
|
|
126
180
|
break;
|
|
127
181
|
case 61: // =
|
|
128
182
|
if (state === HeaderState.START) {
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
if (buffer[0] === 70) {
|
|
184
|
+
// 'File' or 'FilePart'
|
|
185
|
+
let k = 0;
|
|
186
|
+
for (; k < FILE_MARKER.length; ++k) {
|
|
187
|
+
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
|
188
|
+
}
|
|
189
|
+
this.fields['type'] = SequenceType.FILE;
|
|
190
|
+
if (pos === FILEPART_MARKER.length) {
|
|
191
|
+
for (; k < FILEPART_MARKER.length; ++k) {
|
|
192
|
+
if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
|
|
193
|
+
}
|
|
194
|
+
this.fields['type'] = SequenceType.FILEPART;
|
|
195
|
+
this.state = HeaderState.END;
|
|
196
|
+
return i + 1;
|
|
197
|
+
}
|
|
198
|
+
} else if (buffer[0] === 77) {
|
|
199
|
+
// 'MultipartFile'
|
|
200
|
+
for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
|
|
201
|
+
if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
|
|
202
|
+
}
|
|
203
|
+
this.fields['type'] = SequenceType.MULTIPARTFILE;
|
|
204
|
+
} else {
|
|
205
|
+
return this._a();
|
|
131
206
|
}
|
|
132
207
|
state = HeaderState.KEY;
|
|
133
208
|
pos = 0;
|
|
@@ -157,6 +232,7 @@ export class HeaderParser {
|
|
|
157
232
|
}
|
|
158
233
|
|
|
159
234
|
private _a(): number {
|
|
235
|
+
this.fields.type = SequenceType.INVALID;
|
|
160
236
|
this.state = HeaderState.ABORT;
|
|
161
237
|
return -1;
|
|
162
238
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IAddImageOpts } from './Types';
|
|
7
|
+
import { ImageStorage } from './ImageStorage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* IIP (iTerm Image Protocol) specific image storage controller.
|
|
11
|
+
*
|
|
12
|
+
* Wraps the shared ImageStorage with IIP protocol semantics:
|
|
13
|
+
* - Always uses scrolling mode (cursor advances with image)
|
|
14
|
+
*/
|
|
15
|
+
export class IIPImageStorage {
|
|
16
|
+
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly _storage: ImageStorage
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Add an IIP image to storage.
|
|
23
|
+
* Always uses scrolling mode — cursor advances past the image.
|
|
24
|
+
*/
|
|
25
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
|
26
|
+
this._storage.addImage(img, this._addImageOpts);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/IIPMetrics.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
|
|
7
|
+
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
|
|
8
8
|
|
|
9
9
|
export interface IMetrics {
|
|
10
10
|
mime: ImageType;
|
|
@@ -45,6 +45,14 @@ export function imageType(d: Uint8Array): IMetrics {
|
|
|
45
45
|
height: d[9] << 8 | d[8]
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
// QOI: qoif
|
|
49
|
+
if (d32[0] === 0x66696F71) {
|
|
50
|
+
return {
|
|
51
|
+
mime: 'image/qoi',
|
|
52
|
+
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
|
53
|
+
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
48
56
|
return UNSUPPORTED_TYPE;
|
|
49
57
|
}
|
|
50
58
|
|
package/src/ImageAddon.ts
CHANGED
|
@@ -5,24 +5,72 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
|
|
7
7
|
import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
|
8
|
+
import { Emitter, type IEvent } from 'common/Event';
|
|
8
9
|
import { IIPHandler } from './IIPHandler';
|
|
9
10
|
import { ImageRenderer } from './ImageRenderer';
|
|
10
11
|
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
12
|
+
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
|
13
|
+
import { KittyImageStorage } from './kitty/KittyImageStorage';
|
|
11
14
|
import { SixelHandler } from './SixelHandler';
|
|
15
|
+
import { SixelImageStorage } from './SixelImageStorage';
|
|
16
|
+
import { IIPImageStorage } from './IIPImageStorage';
|
|
12
17
|
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
|
13
18
|
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Document VT features provided by this addon.
|
|
22
|
+
*
|
|
23
|
+
* @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
|
|
24
|
+
*
|
|
25
|
+
* Sixel support is provided by the addon @xterm/addon-image with these limitations:
|
|
26
|
+
* - immediate coloring (no shared palette, allows high color settings of `img2sixel`)
|
|
27
|
+
* - max. palette size of 4096 colors
|
|
28
|
+
* - max. pixel width of 16K
|
|
29
|
+
* - max. 25 MB per sixel sequence
|
|
30
|
+
* - VT340 cursor positioning (begin of last sixel data row)
|
|
31
|
+
*
|
|
32
|
+
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image) for more details.
|
|
33
|
+
*
|
|
34
|
+
*
|
|
35
|
+
* @vt: #E[Supported via @xterm/addon-image.] OSC 1337 "iTerm2 Commands" "OSC 1337 ; Pt BEL" "Custom iTerm2 commands."
|
|
36
|
+
*
|
|
37
|
+
* Only the inline image protocol (IIP) is supported by the addon @xterm/addon-image with
|
|
38
|
+
* the following limitations:
|
|
39
|
+
* - sequence:
|
|
40
|
+
* - format: `OSC 1337 ; File=inline=1 ; size=<unencoded size> ; ... : <base64 payload> BEL`
|
|
41
|
+
* - size param must be set and payload may not exceed CEIL(size * 4 / 3)
|
|
42
|
+
* - strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding,
|
|
43
|
+
* no separator bytes allowed)
|
|
44
|
+
* - supported params: size, name, width, height, preserveAspectRatio
|
|
45
|
+
* - image formats: PNG, JPEG and GIF
|
|
46
|
+
* - no animation support (renders first image of a GIF)
|
|
47
|
+
* - no multipart support
|
|
48
|
+
* - VT340 cursor positioning (begin of last sixel data row)
|
|
49
|
+
*
|
|
50
|
+
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image)
|
|
51
|
+
* and [iTerm2 IIP docs](https://iterm2.com/documentation-images.html) for more details.
|
|
52
|
+
*
|
|
53
|
+
*
|
|
54
|
+
* @vt: #E[Supported via @xterm/addon-image.] APC KITTY_GRAPHICS "Kitty Graphics" "APC G Pt ST" "Kitty Graphics Protocol."
|
|
55
|
+
*
|
|
56
|
+
* Kitty graphics support is provided by the addon @xterm/addon-image.
|
|
57
|
+
* Note that while basic image output already works, this is still work in progress.
|
|
58
|
+
*/
|
|
59
|
+
|
|
14
60
|
// default values of addon ctor options
|
|
15
61
|
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
|
16
62
|
enableSizeReports: true,
|
|
17
63
|
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
|
|
18
64
|
sixelSupport: true,
|
|
19
65
|
sixelScrolling: true,
|
|
20
|
-
sixelPaletteLimit:
|
|
21
|
-
sixelSizeLimit:
|
|
66
|
+
sixelPaletteLimit: 4096,
|
|
67
|
+
sixelSizeLimit: 33554432,
|
|
22
68
|
storageLimit: 128,
|
|
23
69
|
showPlaceholder: true,
|
|
24
70
|
iipSupport: true,
|
|
25
|
-
iipSizeLimit:
|
|
71
|
+
iipSizeLimit: 33554432,
|
|
72
|
+
kittySupport: true,
|
|
73
|
+
kittySizeLimit: 33554432
|
|
26
74
|
};
|
|
27
75
|
|
|
28
76
|
// max palette size supported by the sixel lib (compile time setting)
|
|
@@ -48,7 +96,7 @@ const enum GaStatus {
|
|
|
48
96
|
}
|
|
49
97
|
|
|
50
98
|
|
|
51
|
-
export class ImageAddon implements ITerminalAddon
|
|
99
|
+
export class ImageAddon implements ITerminalAddon, IImageApi {
|
|
52
100
|
private _opts: IImageAddonOptions;
|
|
53
101
|
private _defaultOpts: IImageAddonOptions;
|
|
54
102
|
private _storage: ImageStorage | undefined;
|
|
@@ -56,6 +104,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
56
104
|
private _disposables: IDisposable[] = [];
|
|
57
105
|
private _terminal: ITerminalExt | undefined;
|
|
58
106
|
private _handlers: Map<String, IResetHandler> = new Map();
|
|
107
|
+
private readonly _onImageAdded = new Emitter<void>();
|
|
108
|
+
public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
|
|
59
109
|
|
|
60
110
|
constructor(opts?: Partial<IImageAddonOptions>) {
|
|
61
111
|
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
|
@@ -68,6 +118,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
68
118
|
}
|
|
69
119
|
this._disposables.length = 0;
|
|
70
120
|
this._handlers.clear();
|
|
121
|
+
this._onImageAdded.dispose();
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
private _disposeLater(...args: IDisposable[]): void {
|
|
@@ -82,15 +133,11 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
82
133
|
// internal data structures
|
|
83
134
|
this._renderer = new ImageRenderer(terminal);
|
|
84
135
|
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
|
|
136
|
+
this._storage.onImageAdded = () => this._onImageAdded.fire();
|
|
85
137
|
|
|
86
138
|
// enable size reports
|
|
87
139
|
if (this._opts.enableSizeReports) {
|
|
88
|
-
|
|
89
|
-
// windowOptions.getWinSizePixels = true;
|
|
90
|
-
// windowOptions.getCellSizePixels = true;
|
|
91
|
-
// windowOptions.getWinSizeChars = true;
|
|
92
|
-
// terminal.setOption('windowOptions', windowOptions);
|
|
93
|
-
const windowOps = terminal.options.windowOptions || {};
|
|
140
|
+
const windowOps = terminal.options.windowOptions ?? {};
|
|
94
141
|
windowOps.getWinSizePixels = true;
|
|
95
142
|
windowOps.getCellSizePixels = true;
|
|
96
143
|
windowOps.getWinSizeChars = true;
|
|
@@ -129,7 +176,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
129
176
|
|
|
130
177
|
// SIXEL handler
|
|
131
178
|
if (this._opts.sixelSupport) {
|
|
132
|
-
const
|
|
179
|
+
const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
|
|
180
|
+
const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
|
|
133
181
|
this._handlers.set('sixel', sixelHandler);
|
|
134
182
|
this._disposeLater(
|
|
135
183
|
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
|
@@ -138,12 +186,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
138
186
|
|
|
139
187
|
// iTerm IIP handler
|
|
140
188
|
if (this._opts.iipSupport) {
|
|
141
|
-
const
|
|
189
|
+
const iipStorage = new IIPImageStorage(this._storage!);
|
|
190
|
+
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
|
142
191
|
this._handlers.set('iip', iipHandler);
|
|
143
192
|
this._disposeLater(
|
|
144
193
|
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
|
145
194
|
);
|
|
146
195
|
}
|
|
196
|
+
|
|
197
|
+
// Kitty graphics handler
|
|
198
|
+
if (this._opts.kittySupport) {
|
|
199
|
+
const kittyStorage = new KittyImageStorage(this._storage!);
|
|
200
|
+
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
|
|
201
|
+
this._handlers.set('kitty', kittyHandler);
|
|
202
|
+
this._disposeLater(
|
|
203
|
+
kittyStorage,
|
|
204
|
+
kittyHandler,
|
|
205
|
+
terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
147
208
|
}
|
|
148
209
|
|
|
149
210
|
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
|
@@ -194,7 +255,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
|
194
255
|
}
|
|
195
256
|
|
|
196
257
|
private _report(s: string): void {
|
|
197
|
-
this._terminal?._core.
|
|
258
|
+
this._terminal?._core.input(s, false);
|
|
198
259
|
}
|
|
199
260
|
|
|
200
261
|
private _decset(params: (number | number[])[]): boolean {
|