@xterm/addon-image 0.10.0-beta.25 → 0.10.0-beta.251

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.25",
3
+ "version": "0.10.0-beta.251",
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/tsc -p .",
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.0"
28
+ "xterm-wasm-parts": "^0.4.1"
29
29
  },
30
- "commit": "6fe5a124ca8a8e1643a7560aa13bc21005e59445",
30
+ "commit": "c2ecfe744688be7ebe8728004fd8f36717d0de33",
31
31
  "peerDependencies": {
32
- "@xterm/xterm": "^6.1.0-beta.25"
32
+ "@xterm/xterm": "^6.1.0-beta.251"
33
33
  }
34
34
  }
package/src/IIPHandler.ts CHANGED
@@ -4,17 +4,22 @@
4
4
  */
5
5
  import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
6
6
  import { ImageRenderer } from './ImageRenderer';
7
- import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
7
+ import { IIPImageStorage } from './IIPImageStorage';
8
+ import { CELL_SIZE_DEFAULT } from './ImageStorage';
8
9
  import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
10
+ import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
9
11
  import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
10
12
  import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
11
13
 
12
-
13
- // eslint-disable-next-line
14
- declare const Buffer: any;
15
-
16
- // limit hold memory in base64 decoder
17
- const KEEP_DATA = 4194304;
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 = {
@@ -31,15 +36,21 @@ export class IIPHandler implements IOscHandler, IResetHandler {
31
36
  private _aborted = false;
32
37
  private _hp = new HeaderParser();
33
38
  private _header: IHeaderFields = DEFAULT_HEADER;
34
- private _dec = new Base64Decoder(KEEP_DATA);
39
+ private _dec: Base64Decoder;
40
+ private _qoiDec: QoiDecoder;
35
41
  private _metrics = UNSUPPORTED_TYPE;
36
42
 
37
43
  constructor(
38
44
  private readonly _opts: IImageAddonOptions,
39
45
  private readonly _renderer: ImageRenderer,
40
- private readonly _storage: ImageStorage,
46
+ private readonly _storage: IIPImageStorage,
41
47
  private readonly _coreTerminal: ITerminalExt
42
- ) {}
48
+ ) {
49
+ const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
50
+ const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
51
+ this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
52
+ this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
53
+ }
43
54
 
44
55
  public reset(): void {}
45
56
 
@@ -54,7 +65,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
54
65
  if (this._aborted) return;
55
66
 
56
67
  if (this._hp.state === HeaderState.END) {
57
- if (this._dec.put(data, start, end)) {
68
+ if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
58
69
  this._dec.release();
59
70
  this._aborted = true;
60
71
  }
@@ -70,8 +81,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
70
81
  this._aborted = true;
71
82
  return;
72
83
  }
73
- this._dec.init(this._header.size);
74
- if (this._dec.put(data, dataPos, end)) {
84
+ this._dec.init();
85
+ if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
75
86
  this._dec.release();
76
87
  this._aborted = true;
77
88
  }
@@ -89,13 +100,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
89
100
  let cond: number | boolean = true;
90
101
  if (cond = success) {
91
102
  if (cond = !this._dec.end()) {
92
- this._metrics = imageType(this._dec.data8);
93
- if (cond = this._metrics.mime !== 'unsupported') {
94
- w = this._metrics.width;
95
- h = this._metrics.height;
96
- if (cond = w && h && w * h < this._opts.pixelLimit) {
97
- [w, h] = this._resize(w, h).map(Math.floor);
98
- cond = w && h && w * h < this._opts.pixelLimit;
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
+ }
99
112
  }
100
113
  }
101
114
  }
@@ -105,26 +118,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
105
118
  return true;
106
119
  }
107
120
 
108
- const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
109
- this._dec.release();
110
-
111
- if (!window.createImageBitmap) {
112
- const url = URL.createObjectURL(blob);
113
- const img = new Image();
114
- return new Promise<boolean>(r => {
115
- img.addEventListener('load', () => {
116
- URL.revokeObjectURL(url);
117
- const canvas = ImageRenderer.createCanvas(window.document, w, h);
118
- canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
119
- this._storage.addImage(canvas);
120
- r(true);
121
- });
122
- img.src = url;
123
- // sanity measure to avoid terminal blocking from dangling promise
124
- // happens from corrupt data (onload never gets fired)
125
- setTimeout(() => r(true), 1000);
126
- });
121
+ let blob: Blob | ImageData;
122
+ if (this._metrics.mime === 'image/qoi') {
123
+ const data = this._qoiDec.decode(this._dec.data8);
124
+ blob = new ImageData(
125
+ new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
126
+ this._qoiDec.width,
127
+ this._qoiDec.height
128
+ );
129
+ this._qoiDec.release();
130
+ if (w === this._qoiDec.width && h === this._qoiDec.height) {
131
+ // use fast-path if we don't need to rescale
132
+ this._dec.release();
133
+ const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
134
+ canvas.getContext('2d')?.putImageData(blob, 0, 0);
135
+ this._storage.addImage(canvas);
136
+ return true;
137
+ }
138
+ } else {
139
+ blob = new Blob([this._dec.data8], { type: this._metrics.mime });
127
140
  }
141
+ this._dec.release();
128
142
  return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
129
143
  .then(bm => {
130
144
  this._storage.addImage(bm);
@@ -8,6 +8,7 @@ declare const Buffer: any;
8
8
 
9
9
 
10
10
  export interface IHeaderFields {
11
+ [key: string]: number | string | Uint32Array | null | undefined;
11
12
  // base-64 encoded filename. Defaults to "Unnamed file".
12
13
  name: string;
13
14
  // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
81
82
  return new TextDecoder().decode(b);
82
83
  }
83
84
 
84
- const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
85
+ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
85
86
  inline: toInt,
86
87
  size: toInt,
87
88
  name: toName,
@@ -100,7 +101,7 @@ export class HeaderParser {
100
101
  private _buffer = new Uint32Array(MAX_FIELDCHARS);
101
102
  private _position = 0;
102
103
  private _key = '';
103
- public fields: {[key: string]: any} = {};
104
+ public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
104
105
 
105
106
  public reset(): void {
106
107
  this._buffer.fill(0);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { ImageStorage } from './ImageStorage';
7
+
8
+ /**
9
+ * IIP (iTerm Image Protocol) specific image storage controller.
10
+ *
11
+ * Wraps the shared ImageStorage with IIP protocol semantics:
12
+ * - Always uses scrolling mode (cursor advances with image)
13
+ */
14
+ export class IIPImageStorage {
15
+ constructor(
16
+ private readonly _storage: ImageStorage
17
+ ) {}
18
+
19
+ /**
20
+ * Add an IIP image to storage.
21
+ * Always uses scrolling mode — cursor advances past the image.
22
+ */
23
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
24
+ this._storage.addImage(img, true);
25
+ }
26
+ }
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: 256,
21
- sixelSizeLimit: 25000000,
66
+ sixelPaletteLimit: 4096,
67
+ sixelSizeLimit: 33554432,
22
68
  storageLimit: 128,
23
69
  showPlaceholder: true,
24
70
  iipSupport: true,
25
- iipSizeLimit: 20000000
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 , IImageApi {
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,6 +133,7 @@ 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) {
@@ -90,7 +142,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
90
142
  // windowOptions.getCellSizePixels = true;
91
143
  // windowOptions.getWinSizeChars = true;
92
144
  // terminal.setOption('windowOptions', windowOptions);
93
- const windowOps = terminal.options.windowOptions || {};
145
+ const windowOps = terminal.options.windowOptions ?? {};
94
146
  windowOps.getWinSizePixels = true;
95
147
  windowOps.getCellSizePixels = true;
96
148
  windowOps.getWinSizeChars = true;
@@ -129,7 +181,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
129
181
 
130
182
  // SIXEL handler
131
183
  if (this._opts.sixelSupport) {
132
- const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
184
+ const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
185
+ const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
133
186
  this._handlers.set('sixel', sixelHandler);
134
187
  this._disposeLater(
135
188
  terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
@@ -138,12 +191,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
138
191
 
139
192
  // iTerm IIP handler
140
193
  if (this._opts.iipSupport) {
141
- const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
194
+ const iipStorage = new IIPImageStorage(this._storage!);
195
+ const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
142
196
  this._handlers.set('iip', iipHandler);
143
197
  this._disposeLater(
144
198
  terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
145
199
  );
146
200
  }
201
+
202
+ // Kitty graphics handler
203
+ if (this._opts.kittySupport) {
204
+ const kittyStorage = new KittyImageStorage(this._storage!);
205
+ const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
206
+ this._handlers.set('kitty', kittyHandler);
207
+ this._disposeLater(
208
+ kittyStorage,
209
+ kittyHandler,
210
+ terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
211
+ );
212
+ }
147
213
  }
148
214
 
149
215
  // Note: storageLimit is skipped here to not intoduce a surprising side effect.