@xterm/addon-image 0.10.0-beta.252 → 0.10.0-beta.253

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xterm/addon-image",
3
- "version": "0.10.0-beta.252",
3
+ "version": "0.10.0-beta.253",
4
4
  "author": {
5
5
  "name": "The xterm.js authors",
6
6
  "url": "https://xtermjs.org/"
@@ -27,8 +27,8 @@
27
27
  "sixel": "^0.16.0",
28
28
  "xterm-wasm-parts": "^0.4.1"
29
29
  },
30
- "commit": "d3352cf1d5a8c5e1429d737a84d1da810ff13152",
30
+ "commit": "b2003861ecb4acf5b07f49eaadf8403e3e89826c",
31
31
  "peerDependencies": {
32
- "@xterm/xterm": "^6.1.0-beta.252"
32
+ "@xterm/xterm": "^6.1.0-beta.253"
33
33
  }
34
34
  }
package/src/IIPHandler.ts CHANGED
@@ -8,7 +8,7 @@ import { IIPImageStorage } from './IIPImageStorage';
8
8
  import { CELL_SIZE_DEFAULT } from './ImageStorage';
9
9
  import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
10
10
  import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
11
- import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
11
+ import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
12
12
  import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
13
13
 
14
14
  // Local const enum mirror - esbuild can't inline const enums from external packages
@@ -23,6 +23,7 @@ const enum DecoderConst {
23
23
 
24
24
  // default IIP header values
25
25
  const DEFAULT_HEADER: IHeaderFields = {
26
+ type: SequenceType.INVALID,
26
27
  name: 'Unnamed file',
27
28
  size: 0,
28
29
  width: 'auto',
@@ -39,6 +40,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
39
40
  private _dec: Base64Decoder;
40
41
  private _qoiDec: QoiDecoder;
41
42
  private _metrics = UNSUPPORTED_TYPE;
43
+ private _isMultipart = false;
44
+ private _abortMulti = false;
42
45
 
43
46
  constructor(
44
47
  private readonly _opts: IImageAddonOptions,
@@ -52,11 +55,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
52
55
  this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
53
56
  }
54
57
 
55
- public reset(): void {}
58
+ public reset(): void {
59
+ this._hp.reset();
60
+ this._dec.release();
61
+ this._qoiDec.release();
62
+ }
56
63
 
57
64
  public start(): void {
58
65
  this._aborted = false;
59
- this._header = DEFAULT_HEADER;
60
66
  this._metrics = UNSUPPORTED_TYPE;
61
67
  this._hp.reset();
62
68
  }
@@ -76,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
76
82
  return;
77
83
  }
78
84
  if (dataPos > 0) {
79
- this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
80
- if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
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) {
81
99
  this._aborted = true;
82
100
  return;
83
101
  }
84
- this._dec.init();
85
102
  if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
86
103
  this._dec.release();
87
104
  this._aborted = true;
105
+ if (this._isMultipart) this._abortMulti = true;
88
106
  }
89
107
  }
90
108
  }
@@ -93,6 +111,44 @@ export class IIPHandler implements IOscHandler, IResetHandler {
93
111
  public end(success: boolean): boolean | Promise<boolean> {
94
112
  if (this._aborted) return true;
95
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
+
96
152
  let w = 0;
97
153
  let h = 0;
98
154
 
@@ -100,15 +156,13 @@ export class IIPHandler implements IOscHandler, IResetHandler {
100
156
  let cond: number | boolean;
101
157
  if (cond = success) {
102
158
  if (cond = !this._dec.end()) {
103
- if (cond = this._dec.data8.length === this._header.size) {
104
- this._metrics = imageType(this._dec.data8);
105
- if (cond = this._metrics.mime !== 'unsupported') {
106
- w = this._metrics.width;
107
- h = this._metrics.height;
108
- if (cond = w && h && w * h < this._opts.pixelLimit) {
109
- [w, h] = this._resize(w, h).map(Math.floor);
110
- cond = w && h && w * h < this._opts.pixelLimit;
111
- }
159
+ this._metrics = imageType(this._dec.data8);
160
+ if (cond = this._metrics.mime !== 'unsupported') {
161
+ w = this._metrics.width;
162
+ h = this._metrics.height;
163
+ if (cond = w && h && w * h < this._opts.pixelLimit) {
164
+ [w, h] = this._resize(w, h).map(Math.floor);
165
+ cond = w && h && w * h < this._opts.pixelLimit;
112
166
  }
113
167
  }
114
168
  }
@@ -6,9 +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 {
11
27
  [key: string]: number | string | Uint32Array | null | undefined;
28
+ // sequence type
29
+ type: SequenceType;
12
30
  // base-64 encoded filename. Defaults to "Unnamed file".
13
31
  name: string;
14
32
  // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -29,14 +47,6 @@ export interface IHeaderFields {
29
47
  inline?: number;
30
48
  }
31
49
 
32
- export const enum HeaderState {
33
- START = 0,
34
- ABORT = 1,
35
- KEY = 2,
36
- VALUE = 3,
37
- END = 4
38
- }
39
-
40
50
  // field value decoders
41
51
 
42
52
  // ASCII bytes to string
@@ -92,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
92
102
  };
93
103
 
94
104
 
105
+ // sequence type markers
106
+ // File
95
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
96
118
  const MAX_FIELDCHARS = 1024;
97
119
 
98
120
 
@@ -111,12 +133,43 @@ export class HeaderParser {
111
133
  this._key = '';
112
134
  }
113
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
+
114
167
  public parse(data: Uint32Array, start: number, end: number): number {
115
168
  let state = this.state;
116
169
  let pos = this._position;
117
170
  const buffer = this._buffer;
118
171
  if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
119
- if (state === HeaderState.START && pos > 6) return -1;
172
+ if (state === HeaderState.START && pos > 14) return -1;
120
173
  for (let i = start; i < end; ++i) {
121
174
  const c = data[i];
122
175
  switch (c) {
@@ -127,8 +180,29 @@ export class HeaderParser {
127
180
  break;
128
181
  case 61: // =
129
182
  if (state === HeaderState.START) {
130
- for (let k = 0; k < FILE_MARKER.length; ++k) {
131
- if (buffer[k] !== FILE_MARKER[k]) return this._a();
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();
132
206
  }
133
207
  state = HeaderState.KEY;
134
208
  pos = 0;
@@ -158,6 +232,7 @@ export class HeaderParser {
158
232
  }
159
233
 
160
234
  private _a(): number {
235
+ this.fields.type = SequenceType.INVALID;
161
236
  this.state = HeaderState.ABORT;
162
237
  return -1;
163
238
  }
@@ -3,6 +3,7 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
+ import { IAddImageOpts } from 'Types';
6
7
  import { ImageStorage } from './ImageStorage';
7
8
 
8
9
  /**
@@ -12,6 +13,7 @@ import { ImageStorage } from './ImageStorage';
12
13
  * - Always uses scrolling mode (cursor advances with image)
13
14
  */
14
15
  export class IIPImageStorage {
16
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
15
17
  constructor(
16
18
  private readonly _storage: ImageStorage
17
19
  ) {}
@@ -21,6 +23,6 @@ export class IIPImageStorage {
21
23
  * Always uses scrolling mode — cursor advances past the image.
22
24
  */
23
25
  public addImage(img: HTMLCanvasElement | ImageBitmap): void {
24
- this._storage.addImage(img, true);
26
+ this._storage.addImage(img, this._addImageOpts);
25
27
  }
26
28
  }
package/src/ImageAddon.ts CHANGED
@@ -137,11 +137,6 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
137
137
 
138
138
  // enable size reports
139
139
  if (this._opts.enableSizeReports) {
140
- // const windowOptions = terminal.getOption('windowOptions');
141
- // windowOptions.getWinSizePixels = true;
142
- // windowOptions.getCellSizePixels = true;
143
- // windowOptions.getWinSizeChars = true;
144
- // terminal.setOption('windowOptions', windowOptions);
145
140
  const windowOps = terminal.options.windowOptions ?? {};
146
141
  windowOps.getWinSizePixels = true;
147
142
  windowOps.getCellSizePixels = true;
@@ -260,7 +255,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
260
255
  }
261
256
 
262
257
  private _report(s: string): void {
263
- this._terminal?._core.coreService.triggerDataEvent(s);
258
+ this._terminal?._core.input(s, false);
264
259
  }
265
260
 
266
261
  private _decset(params: (number | number[])[]): boolean {
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { IDisposable } from '@xterm/xterm';
7
7
  import { ImageRenderer } from './ImageRenderer';
8
- import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
8
+ import {
9
+ ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
10
+ IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
11
+ UnderlineStyle, IAddImageOpts
12
+ } from './Types';
9
13
 
10
14
 
11
15
  // fallback default cell size
@@ -235,13 +239,15 @@ export class ImageStorage implements IDisposable {
235
239
  /**
236
240
  * Method to add an image to the storage.
237
241
  * @param img - The image to add (canvas or bitmap).
238
- * @param scrolling - When true, cursor advances with the image (lineFeed per row).
239
- * When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
240
- * @param layer - Which canvas layer to render on ('top' or 'bottom').
241
- * @param zIndex - Z-index for image layering within the same layer.
242
+ * @param opts - Options for addImage:
243
+ * - scrolling: When true, cursor advances with the image.
244
+ * When false, image is placed at ORIGIN and cursor does not move.
245
+ * - layer: Which canvas layer to render on ('top' or 'bottom').
246
+ * - zIndex: Z-index for image layering within the same layer.
247
+ * - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
242
248
  * @returns The internal image ID assigned to the stored image.
243
249
  */
244
- public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
250
+ public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
245
251
  // never allow storage to exceed memory limit
246
252
  this._evictOldest(img.width * img.height);
247
253
 
@@ -263,7 +269,7 @@ export class ImageStorage implements IDisposable {
263
269
  let offset = originX;
264
270
  let tileCount = 0;
265
271
 
266
- if (!scrolling) {
272
+ if (!opts.scrolling) {
267
273
  buffer.x = 0;
268
274
  buffer.y = 0;
269
275
  offset = 0;
@@ -277,7 +283,7 @@ export class ImageStorage implements IDisposable {
277
283
  this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
278
284
  tileCount++;
279
285
  }
280
- if (scrolling) {
286
+ if (opts.scrolling) {
281
287
  if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
282
288
  } else {
283
289
  if (++buffer.y >= termRows) break;
@@ -287,8 +293,12 @@ export class ImageStorage implements IDisposable {
287
293
  this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
288
294
 
289
295
  // cursor positioning modes
290
- if (scrolling) {
291
- buffer.x = offset;
296
+ if (opts.scrolling) {
297
+ if (opts.cursorPos === 'iip') {
298
+ buffer.x = Math.min(offset + cols, termCols);
299
+ } else {
300
+ buffer.x = offset;
301
+ }
292
302
  } else {
293
303
  buffer.x = originX;
294
304
  buffer.y = originY;
@@ -331,8 +341,8 @@ export class ImageStorage implements IDisposable {
331
341
  marker: endMarker || undefined,
332
342
  tileCount,
333
343
  bufferType: this._terminal.buffer.active.type,
334
- layer,
335
- zIndex
344
+ layer: opts.layer,
345
+ zIndex: opts.zIndex
336
346
  };
337
347
 
338
348
  // finally add the image
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
7
- import { IImageAddonOptions, ITerminalExt } from './Types';
7
+ import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
8
8
  import { ImageRenderer } from './ImageRenderer';
9
9
 
10
10
  /**
@@ -15,6 +15,7 @@ import { ImageRenderer } from './ImageRenderer';
15
15
  * - advanceCursor for empty sixels carrying only height
16
16
  */
17
17
  export class SixelImageStorage {
18
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'vt340' };
18
19
  constructor(
19
20
  private readonly _storage: ImageStorage,
20
21
  private readonly _opts: IImageAddonOptions,
@@ -27,7 +28,8 @@ export class SixelImageStorage {
27
28
  * Cursor behavior depends on the sixelScrolling option (DECSET 80).
28
29
  */
29
30
  public addImage(img: HTMLCanvasElement | ImageBitmap): void {
30
- this._storage.addImage(img, this._opts.sixelScrolling);
31
+ this._addImageOpts.scrolling = this._opts.sixelScrolling;
32
+ this._storage.addImage(img, this._addImageOpts);
31
33
  }
32
34
 
33
35
  /**
package/src/Types.ts CHANGED
@@ -100,6 +100,14 @@ export interface ICellSize {
100
100
  }
101
101
 
102
102
  export type ImageLayer = 'top' | 'bottom';
103
+ export type CursorPos = 'vt340' | 'iip';
104
+
105
+ export interface IAddImageOpts {
106
+ scrolling: boolean;
107
+ layer: ImageLayer;
108
+ zIndex: number;
109
+ cursorPos: CursorPos;
110
+ }
103
111
 
104
112
  export interface IImageSpec {
105
113
  orig: HTMLCanvasElement | ImageBitmap | undefined;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { IDisposable } from '@xterm/xterm';
7
7
  import { ImageStorage } from '../ImageStorage';
8
- import { ImageLayer } from '../Types';
8
+ import { ImageLayer, IAddImageOpts } from '../Types';
9
9
  import { IKittyImageData } from './KittyGraphicsTypes';
10
10
 
11
11
  // Kitty-specific image storage controller.
@@ -42,6 +42,7 @@ export class KittyImageStorage implements IDisposable {
42
42
  this._images.delete(kittyId);
43
43
  }
44
44
  };
45
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
45
46
 
46
47
  constructor(
47
48
  private readonly _storage: ImageStorage
@@ -98,7 +99,10 @@ export class KittyImageStorage implements IDisposable {
98
99
  if (oldStorageId !== undefined) {
99
100
  this._storageIdToKittyId.delete(oldStorageId);
100
101
  }
101
- const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
102
+ this._addImageOpts.scrolling = scrolling;
103
+ this._addImageOpts.layer = layer;
104
+ this._addImageOpts.zIndex = zIndex;
105
+ const storageId = this._storage.addImage(image, this._addImageOpts);
102
106
  this._kittyIdToStorageId.set(kittyId, storageId);
103
107
  this._storageIdToKittyId.set(storageId, kittyId);
104
108
  }