@xterm/addon-image 0.10.0-beta.3 → 0.10.0-beta.300
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 +40 -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 +124 -47
- package/src/IIPHeaderParser.ts +90 -14
- package/src/IIPImageStorage.ts +28 -0
- package/src/IIPMetrics.ts +71 -5
- package/src/ImageAddon.ts +74 -13
- package/src/ImageRenderer.ts +99 -53
- package/src/ImageStorage.ts +103 -39
- 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.300",
|
|
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": "d3e32b344dfe7dd6015cff6a9aeaaeaeccdc2789",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
32
|
+
"@xterm/xterm": "^6.1.0-beta.303"
|
|
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,22 +37,31 @@ 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
|
|
35
|
-
private
|
|
40
|
+
private _dec: Base64Decoder;
|
|
41
|
+
private _qoiDec: QoiDecoder;
|
|
42
|
+
private _isMultipart = false;
|
|
43
|
+
private _abortMulti = false;
|
|
36
44
|
|
|
37
45
|
constructor(
|
|
38
46
|
private readonly _opts: IImageAddonOptions,
|
|
39
47
|
private readonly _renderer: ImageRenderer,
|
|
40
|
-
private readonly _storage:
|
|
48
|
+
private readonly _storage: IIPImageStorage,
|
|
41
49
|
private readonly _coreTerminal: ITerminalExt
|
|
42
|
-
) {
|
|
50
|
+
) {
|
|
51
|
+
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
|
52
|
+
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
|
53
|
+
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
|
|
54
|
+
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
|
|
55
|
+
}
|
|
43
56
|
|
|
44
|
-
public reset(): void {
|
|
57
|
+
public reset(): void {
|
|
58
|
+
this._hp.reset();
|
|
59
|
+
this._dec.release();
|
|
60
|
+
this._qoiDec.release();
|
|
61
|
+
}
|
|
45
62
|
|
|
46
63
|
public start(): void {
|
|
47
64
|
this._aborted = false;
|
|
48
|
-
this._header = DEFAULT_HEADER;
|
|
49
|
-
this._metrics = UNSUPPORTED_TYPE;
|
|
50
65
|
this._hp.reset();
|
|
51
66
|
}
|
|
52
67
|
|
|
@@ -54,7 +69,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
54
69
|
if (this._aborted) return;
|
|
55
70
|
|
|
56
71
|
if (this._hp.state === HeaderState.END) {
|
|
57
|
-
if (this._dec.put(data
|
|
72
|
+
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
|
|
58
73
|
this._dec.release();
|
|
59
74
|
this._aborted = true;
|
|
60
75
|
}
|
|
@@ -65,15 +80,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
65
80
|
return;
|
|
66
81
|
}
|
|
67
82
|
if (dataPos > 0) {
|
|
68
|
-
|
|
69
|
-
if (
|
|
83
|
+
const seqType = this._hp.fields.type;
|
|
84
|
+
if (seqType === SequenceType.FILE) {
|
|
85
|
+
if (this._isMultipart) {
|
|
86
|
+
this._isMultipart = false;
|
|
87
|
+
this._abortMulti = false;
|
|
88
|
+
this._dec.release();
|
|
89
|
+
}
|
|
90
|
+
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
|
91
|
+
if (!this._header.inline) {
|
|
92
|
+
this._aborted = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this._dec.init();
|
|
96
|
+
} else if (this._abortMulti) {
|
|
70
97
|
this._aborted = true;
|
|
71
98
|
return;
|
|
72
99
|
}
|
|
73
|
-
this._dec.
|
|
74
|
-
if (this._dec.put(data, dataPos, end)) {
|
|
100
|
+
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
|
75
101
|
this._dec.release();
|
|
76
102
|
this._aborted = true;
|
|
103
|
+
if (this._isMultipart) this._abortMulti = true;
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
}
|
|
@@ -82,22 +109,67 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
82
109
|
public end(success: boolean): boolean | Promise<boolean> {
|
|
83
110
|
if (this._aborted) return true;
|
|
84
111
|
|
|
112
|
+
if (this._hp.state !== HeaderState.END) {
|
|
113
|
+
if (this._hp.end()) return true;
|
|
114
|
+
}
|
|
115
|
+
const seqType = this._hp.fields.type;
|
|
116
|
+
|
|
117
|
+
if (seqType === SequenceType.FILEPART) return true;
|
|
118
|
+
|
|
119
|
+
if (seqType === SequenceType.REPORTCELLSIZE) {
|
|
120
|
+
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
|
121
|
+
let w = CELL_SIZE_DEFAULT.width;
|
|
122
|
+
let h = CELL_SIZE_DEFAULT.height;
|
|
123
|
+
if (this._renderer.dimensions) {
|
|
124
|
+
w = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
|
125
|
+
h = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
|
126
|
+
}
|
|
127
|
+
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
|
128
|
+
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
|
129
|
+
this._coreTerminal.input(report, false);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (seqType === SequenceType.MULTIPARTFILE) {
|
|
134
|
+
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
|
135
|
+
this._isMultipart = true;
|
|
136
|
+
this._abortMulti = false;
|
|
137
|
+
this._dec.release();
|
|
138
|
+
this._dec.init();
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (seqType === SequenceType.FILEEND) {
|
|
143
|
+
if (!this._isMultipart) return true;
|
|
144
|
+
this._isMultipart = false;
|
|
145
|
+
if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
|
149
|
+
|
|
85
150
|
let w = 0;
|
|
86
151
|
let h = 0;
|
|
87
152
|
|
|
88
153
|
// early exit condition chain
|
|
89
|
-
let cond: number | boolean
|
|
154
|
+
let cond: number | boolean;
|
|
155
|
+
let metrics = UNSUPPORTED_TYPE;
|
|
90
156
|
if (cond = success) {
|
|
91
157
|
if (cond = !this._dec.end()) {
|
|
92
|
-
|
|
93
|
-
if (cond =
|
|
94
|
-
w =
|
|
95
|
-
h =
|
|
158
|
+
metrics = imageType(this._dec.data8);
|
|
159
|
+
if (cond = metrics.mime !== 'unsupported') {
|
|
160
|
+
w = metrics.width;
|
|
161
|
+
h = metrics.height;
|
|
96
162
|
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
|
97
163
|
[w, h] = this._resize(w, h).map(Math.floor);
|
|
98
164
|
cond = w && h && w * h < this._opts.pixelLimit;
|
|
165
|
+
} else {
|
|
166
|
+
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
|
|
99
167
|
}
|
|
168
|
+
} else {
|
|
169
|
+
console.warn('IIP: unsupported image type');
|
|
100
170
|
}
|
|
171
|
+
} else {
|
|
172
|
+
console.warn('IIP: error during BASE64 decoding');
|
|
101
173
|
}
|
|
102
174
|
}
|
|
103
175
|
if (!cond) {
|
|
@@ -105,30 +177,35 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
105
177
|
return true;
|
|
106
178
|
}
|
|
107
179
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
180
|
+
let blob: Blob | ImageData;
|
|
181
|
+
if (metrics.mime === 'image/qoi') {
|
|
182
|
+
const data = this._qoiDec.decode(this._dec.data8);
|
|
183
|
+
blob = new ImageData(
|
|
184
|
+
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
|
185
|
+
this._qoiDec.width,
|
|
186
|
+
this._qoiDec.height
|
|
187
|
+
);
|
|
188
|
+
this._qoiDec.release();
|
|
189
|
+
if (w === this._qoiDec.width && h === this._qoiDec.height) {
|
|
190
|
+
// use fast-path if we don't need to rescale
|
|
191
|
+
this._dec.release();
|
|
192
|
+
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
|
|
193
|
+
canvas.getContext('2d')?.putImageData(blob, 0, 0);
|
|
194
|
+
this._storage.addImage(canvas);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
blob = new Blob([this._dec.data8], { type: metrics.mime });
|
|
127
199
|
}
|
|
200
|
+
this._dec.release();
|
|
128
201
|
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
|
129
202
|
.then(bm => {
|
|
130
203
|
this._storage.addImage(bm);
|
|
131
204
|
return true;
|
|
205
|
+
})
|
|
206
|
+
.catch(e => {
|
|
207
|
+
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
|
|
208
|
+
return true;
|
|
132
209
|
});
|
|
133
210
|
}
|
|
134
211
|
|
|
@@ -154,8 +231,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|
|
154
231
|
|
|
155
232
|
private _dim(s: string, total: number, cdim: number): number {
|
|
156
233
|
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;
|
|
234
|
+
if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
|
|
235
|
+
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
|
|
236
|
+
return parseInt(s, 10) * cdim;
|
|
160
237
|
}
|
|
161
238
|
}
|
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
|
}
|
|
@@ -176,7 +252,7 @@ export class HeaderParser {
|
|
|
176
252
|
try {
|
|
177
253
|
const v = this._buffer.slice(0, pos);
|
|
178
254
|
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
|
|
179
|
-
} catch
|
|
255
|
+
} catch {
|
|
180
256
|
return false;
|
|
181
257
|
}
|
|
182
258
|
return true;
|
|
@@ -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' | 'image/webp' | 'image/avif' | 'unsupported' | '';
|
|
8
8
|
|
|
9
9
|
export interface IMetrics {
|
|
10
10
|
mime: ImageType;
|
|
@@ -19,16 +19,16 @@ export const UNSUPPORTED_TYPE: IMetrics = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export function imageType(d: Uint8Array): IMetrics {
|
|
22
|
-
if (d.length <
|
|
22
|
+
if (d.length < 32) {
|
|
23
23
|
return UNSUPPORTED_TYPE;
|
|
24
24
|
}
|
|
25
|
-
const d32 = new Uint32Array(d.buffer, d.byteOffset,
|
|
25
|
+
const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
|
|
26
26
|
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
|
27
27
|
// + first chunk must be IHDR
|
|
28
28
|
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
|
29
29
|
return {
|
|
30
30
|
mime: 'image/png',
|
|
31
|
-
width:
|
|
31
|
+
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
|
32
32
|
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
|
33
33
|
};
|
|
34
34
|
}
|
|
@@ -41,13 +41,79 @@ export function imageType(d: Uint8Array): IMetrics {
|
|
|
41
41
|
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
|
42
42
|
return {
|
|
43
43
|
mime: 'image/gif',
|
|
44
|
-
width:
|
|
44
|
+
width: d[7] << 8 | d[6],
|
|
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
|
+
}
|
|
56
|
+
// WEBP: RIFF | xxxx | WEBP | VP8x
|
|
57
|
+
if (d32[0] === 0x46464952 && d32[2] === 0x50424557 && (d32[3] & 0xFFFFFF) === 0x385056) {
|
|
58
|
+
switch (d[15]) {
|
|
59
|
+
case 0x58: // Extended WebP VP8X --> "X"
|
|
60
|
+
return {
|
|
61
|
+
mime: 'image/webp',
|
|
62
|
+
width: (d[24] | d[25] << 8 | d[26] << 16) + 1,
|
|
63
|
+
height: (d[27] | d[28] << 8 | d[29] << 16) + 1
|
|
64
|
+
};
|
|
65
|
+
case 0x4C: // Lossless WebP VP8L --> "L"
|
|
66
|
+
if (d[20] !== 0x2f) return UNSUPPORTED_TYPE;
|
|
67
|
+
const dim = d[21] | d[22] << 8 | d[23] << 16 | d[24] << 24;
|
|
68
|
+
return {
|
|
69
|
+
mime: 'image/webp',
|
|
70
|
+
width: (dim & 0x3FFF) + 1,
|
|
71
|
+
height: (dim >>> 14 & 0x3FFF) + 1
|
|
72
|
+
};
|
|
73
|
+
case 0x20: // Lossy WebP VP8 --> " "
|
|
74
|
+
if (d[23] !== 0x9d || d[24] !== 0x01 || d[25] !== 0x2a) return UNSUPPORTED_TYPE;
|
|
75
|
+
return {
|
|
76
|
+
mime: 'image/webp',
|
|
77
|
+
width: (d[26] | d[27] << 8) & 0x3FFF,
|
|
78
|
+
height: (d[28] | d[29] << 8) & 0x3FFF
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return UNSUPPORTED_TYPE;
|
|
82
|
+
}
|
|
83
|
+
// AVIF: Box size | ftyp | avif/avis
|
|
84
|
+
if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
|
|
85
|
+
let pos = -1;
|
|
86
|
+
// search for ispe box within first 1024 bytes
|
|
87
|
+
const limit = Math.min(d.length - 16, 1024);
|
|
88
|
+
for (let i = 8; i < limit; i++) {
|
|
89
|
+
// scan for ispe
|
|
90
|
+
if (d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
|
|
91
|
+
pos = i;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (pos !== -1) {
|
|
96
|
+
// dimensions are in BE at +8 (width) at +12 (height)
|
|
97
|
+
const width =
|
|
98
|
+
d[pos + 8] << 24 |
|
|
99
|
+
d[pos + 9] << 16 |
|
|
100
|
+
d[pos + 10] << 8 |
|
|
101
|
+
d[pos + 11];
|
|
102
|
+
const height =
|
|
103
|
+
d[pos + 12] << 24 |
|
|
104
|
+
d[pos + 13] << 16 |
|
|
105
|
+
d[pos + 14] << 8 |
|
|
106
|
+
d[pos + 15];
|
|
107
|
+
if (width > 0 && height > 0) {
|
|
108
|
+
return { mime: 'image/avif', width, height };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return UNSUPPORTED_TYPE;
|
|
112
|
+
}
|
|
48
113
|
return UNSUPPORTED_TYPE;
|
|
49
114
|
}
|
|
50
115
|
|
|
116
|
+
|
|
51
117
|
function jpgSize(d: Uint8Array): [number, number] {
|
|
52
118
|
const len = d.length;
|
|
53
119
|
let i = 4;
|