@sailfish-ai/addon-image 0.9.0
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/LICENSE +19 -0
- package/README.md +231 -0
- package/lib/addon-image.js +3 -0
- package/lib/addon-image.js.map +1 -0
- package/lib/addon-image.mjs +39 -0
- package/lib/addon-image.mjs.map +7 -0
- package/package.json +34 -0
- package/src/IIPHandler.ts +161 -0
- package/src/IIPHeaderParser.ts +186 -0
- package/src/IIPMetrics.ts +76 -0
- package/src/ImageAddon.ts +317 -0
- package/src/ImageRenderer.ts +378 -0
- package/src/ImageStorage.ts +604 -0
- package/src/SixelHandler.ts +151 -0
- package/src/Types.ts +108 -0
- package/typings/addon-image.d.ts +120 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ITerminalAddon, IDisposable } from '@sailfish-ai/xterm';
|
|
7
|
+
import type { ImageAddon as IImageApi } from '@sailfish-ai/addon-image';
|
|
8
|
+
import { IIPHandler } from './IIPHandler';
|
|
9
|
+
import { ImageRenderer } from './ImageRenderer';
|
|
10
|
+
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
11
|
+
import { SixelHandler } from './SixelHandler';
|
|
12
|
+
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
|
13
|
+
|
|
14
|
+
// default values of addon ctor options
|
|
15
|
+
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
|
16
|
+
enableSizeReports: true,
|
|
17
|
+
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
|
|
18
|
+
sixelSupport: true,
|
|
19
|
+
sixelScrolling: true,
|
|
20
|
+
sixelPaletteLimit: 256,
|
|
21
|
+
sixelSizeLimit: 25000000,
|
|
22
|
+
storageLimit: 128,
|
|
23
|
+
showPlaceholder: true,
|
|
24
|
+
iipSupport: true,
|
|
25
|
+
iipSizeLimit: 20000000
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// max palette size supported by the sixel lib (compile time setting)
|
|
29
|
+
const MAX_SIXEL_PALETTE_SIZE = 4096;
|
|
30
|
+
|
|
31
|
+
// definitions for _xtermGraphicsAttributes sequence
|
|
32
|
+
const enum GaItem {
|
|
33
|
+
COLORS = 1,
|
|
34
|
+
SIXEL_GEO = 2,
|
|
35
|
+
REGIS_GEO = 3
|
|
36
|
+
}
|
|
37
|
+
const enum GaAction {
|
|
38
|
+
READ = 1,
|
|
39
|
+
SET_DEFAULT = 2,
|
|
40
|
+
SET = 3,
|
|
41
|
+
READ_MAX = 4
|
|
42
|
+
}
|
|
43
|
+
const enum GaStatus {
|
|
44
|
+
SUCCESS = 0,
|
|
45
|
+
ITEM_ERROR = 1,
|
|
46
|
+
ACTION_ERROR = 2,
|
|
47
|
+
FAILURE = 3
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export class ImageAddon implements ITerminalAddon , IImageApi {
|
|
52
|
+
private _opts: IImageAddonOptions;
|
|
53
|
+
private _defaultOpts: IImageAddonOptions;
|
|
54
|
+
private _storage: ImageStorage | undefined;
|
|
55
|
+
private _renderer: ImageRenderer | undefined;
|
|
56
|
+
private _disposables: IDisposable[] = [];
|
|
57
|
+
private _terminal: ITerminalExt | undefined;
|
|
58
|
+
private _handlers: Map<String, IResetHandler> = new Map();
|
|
59
|
+
|
|
60
|
+
constructor(opts?: Partial<IImageAddonOptions>) {
|
|
61
|
+
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
|
62
|
+
this._defaultOpts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public dispose(): void {
|
|
66
|
+
for (const obj of this._disposables) {
|
|
67
|
+
obj.dispose();
|
|
68
|
+
}
|
|
69
|
+
this._disposables.length = 0;
|
|
70
|
+
this._handlers.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _disposeLater(...args: IDisposable[]): void {
|
|
74
|
+
for (const obj of args) {
|
|
75
|
+
this._disposables.push(obj);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public activate(terminal: ITerminalExt): void {
|
|
80
|
+
this._terminal = terminal;
|
|
81
|
+
|
|
82
|
+
// internal data structures
|
|
83
|
+
this._renderer = new ImageRenderer(terminal);
|
|
84
|
+
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
|
|
85
|
+
|
|
86
|
+
// enable size reports
|
|
87
|
+
if (this._opts.enableSizeReports) {
|
|
88
|
+
// const windowOptions = terminal.getOption('windowOptions');
|
|
89
|
+
// windowOptions.getWinSizePixels = true;
|
|
90
|
+
// windowOptions.getCellSizePixels = true;
|
|
91
|
+
// windowOptions.getWinSizeChars = true;
|
|
92
|
+
// terminal.setOption('windowOptions', windowOptions);
|
|
93
|
+
const windowOps = terminal.options.windowOptions || {};
|
|
94
|
+
windowOps.getWinSizePixels = true;
|
|
95
|
+
windowOps.getCellSizePixels = true;
|
|
96
|
+
windowOps.getWinSizeChars = true;
|
|
97
|
+
terminal.options.windowOptions = windowOps;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._disposeLater(
|
|
101
|
+
this._renderer,
|
|
102
|
+
this._storage,
|
|
103
|
+
|
|
104
|
+
// DECSET/DECRST/DA1/XTSMGRAPHICS handlers
|
|
105
|
+
terminal.parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this._decset(params)),
|
|
106
|
+
terminal.parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this._decrst(params)),
|
|
107
|
+
terminal.parser.registerCsiHandler({ final: 'c' }, params => this._da1(params)),
|
|
108
|
+
terminal.parser.registerCsiHandler({ prefix: '?', final: 'S' }, params => this._xtermGraphicsAttributes(params)),
|
|
109
|
+
|
|
110
|
+
// render hook
|
|
111
|
+
terminal.onRender(range => this._storage?.render(range)),
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* reset handlers covered:
|
|
115
|
+
* - DECSTR
|
|
116
|
+
* - RIS
|
|
117
|
+
* - Terminal.reset()
|
|
118
|
+
*/
|
|
119
|
+
terminal.parser.registerCsiHandler({ intermediates: '!', final: 'p' }, () => this.reset()),
|
|
120
|
+
terminal.parser.registerEscHandler({ final: 'c' }, () => this.reset()),
|
|
121
|
+
terminal._core._inputHandler.onRequestReset(() => this.reset()),
|
|
122
|
+
|
|
123
|
+
// wipe canvas and delete alternate images on buffer switch
|
|
124
|
+
terminal.buffer.onBufferChange(() => this._storage?.wipeAlternate()),
|
|
125
|
+
|
|
126
|
+
// extend images to the right on resize
|
|
127
|
+
terminal.onResize(metrics => this._storage?.viewportResize(metrics))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// SIXEL handler
|
|
131
|
+
if (this._opts.sixelSupport) {
|
|
132
|
+
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
|
|
133
|
+
this._handlers.set('sixel', sixelHandler);
|
|
134
|
+
this._disposeLater(
|
|
135
|
+
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// iTerm IIP handler
|
|
140
|
+
if (this._opts.iipSupport) {
|
|
141
|
+
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
|
|
142
|
+
this._handlers.set('iip', iipHandler);
|
|
143
|
+
this._disposeLater(
|
|
144
|
+
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
|
150
|
+
public reset(): boolean {
|
|
151
|
+
// reset options customizable by sequences to defaults
|
|
152
|
+
this._opts.sixelScrolling = this._defaultOpts.sixelScrolling;
|
|
153
|
+
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
|
|
154
|
+
// also clear image storage
|
|
155
|
+
this._storage?.reset();
|
|
156
|
+
// reset protocol handlers
|
|
157
|
+
for (const handler of this._handlers.values()) {
|
|
158
|
+
handler.reset();
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public get storageLimit(): number {
|
|
164
|
+
return this._storage?.getLimit() || -1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public set storageLimit(limit: number) {
|
|
168
|
+
this._storage?.setLimit(limit);
|
|
169
|
+
this._opts.storageLimit = limit;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public get storageUsage(): number {
|
|
173
|
+
if (this._storage) {
|
|
174
|
+
return this._storage.getUsage();
|
|
175
|
+
}
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public get showPlaceholder(): boolean {
|
|
180
|
+
return this._opts.showPlaceholder;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public set showPlaceholder(value: boolean) {
|
|
184
|
+
this._opts.showPlaceholder = value;
|
|
185
|
+
this._renderer?.showPlaceholder(value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
189
|
+
return this._storage?.getImageAtBufferCell(x, y);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
193
|
+
return this._storage?.extractTileAtBufferCell(x, y);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private _report(s: string): void {
|
|
197
|
+
this._terminal?._core.coreService.triggerDataEvent(s);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _decset(params: (number | number[])[]): boolean {
|
|
201
|
+
for (let i = 0; i < params.length; ++i) {
|
|
202
|
+
switch (params[i]) {
|
|
203
|
+
case 80:
|
|
204
|
+
this._opts.sixelScrolling = false;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _decrst(params: (number | number[])[]): boolean {
|
|
212
|
+
for (let i = 0; i < params.length; ++i) {
|
|
213
|
+
switch (params[i]) {
|
|
214
|
+
case 80:
|
|
215
|
+
this._opts.sixelScrolling = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// overload DA to return something more appropriate
|
|
223
|
+
private _da1(params: (number | number[])[]): boolean {
|
|
224
|
+
if (params[0]) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
// reported features:
|
|
228
|
+
// 62 - VT220
|
|
229
|
+
// 4 - SIXEL support
|
|
230
|
+
// 9 - charsets
|
|
231
|
+
// 22 - ANSI colors
|
|
232
|
+
if (this._opts.sixelSupport) {
|
|
233
|
+
this._report(`\x1b[?62;4;9;22c`);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Implementation of xterm's graphics attribute sequence.
|
|
241
|
+
*
|
|
242
|
+
* Supported features:
|
|
243
|
+
* - read/change palette limits (max 4096 by sixel lib)
|
|
244
|
+
* - read SIXEL canvas geometry (reports current window canvas or
|
|
245
|
+
* squared pixelLimit if canvas > pixel limit)
|
|
246
|
+
*
|
|
247
|
+
* Everything else is deactivated.
|
|
248
|
+
*/
|
|
249
|
+
private _xtermGraphicsAttributes(params: (number | number[])[]): boolean {
|
|
250
|
+
if (params.length < 2) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (params[0] === GaItem.COLORS) {
|
|
254
|
+
switch (params[1]) {
|
|
255
|
+
case GaAction.READ:
|
|
256
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
|
257
|
+
return true;
|
|
258
|
+
case GaAction.SET_DEFAULT:
|
|
259
|
+
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
|
|
260
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
|
261
|
+
// also reset protocol handlers for now
|
|
262
|
+
for (const handler of this._handlers.values()) {
|
|
263
|
+
handler.reset();
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
case GaAction.SET:
|
|
267
|
+
if (params.length > 2 && !(params[2] instanceof Array) && params[2] <= MAX_SIXEL_PALETTE_SIZE) {
|
|
268
|
+
this._opts.sixelPaletteLimit = params[2];
|
|
269
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
|
270
|
+
} else {
|
|
271
|
+
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
case GaAction.READ_MAX:
|
|
275
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${MAX_SIXEL_PALETTE_SIZE}S`);
|
|
276
|
+
return true;
|
|
277
|
+
default:
|
|
278
|
+
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (params[0] === GaItem.SIXEL_GEO) {
|
|
283
|
+
switch (params[1]) {
|
|
284
|
+
// we only implement read and read_max here
|
|
285
|
+
case GaAction.READ:
|
|
286
|
+
let width = this._renderer?.dimensions?.css.canvas.width;
|
|
287
|
+
let height = this._renderer?.dimensions?.css.canvas.height;
|
|
288
|
+
if (!width || !height) {
|
|
289
|
+
// for some reason we have no working image renderer
|
|
290
|
+
// --> fallback to default cell size
|
|
291
|
+
const cellSize = CELL_SIZE_DEFAULT;
|
|
292
|
+
width = (this._terminal?.cols || 80) * cellSize.width;
|
|
293
|
+
height = (this._terminal?.rows || 24) * cellSize.height;
|
|
294
|
+
}
|
|
295
|
+
if (width * height < this._opts.pixelLimit) {
|
|
296
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${width.toFixed(0)};${height.toFixed(0)}S`);
|
|
297
|
+
} else {
|
|
298
|
+
// if we overflow pixelLimit report that squared instead
|
|
299
|
+
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
|
|
300
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
case GaAction.READ_MAX:
|
|
304
|
+
// read_max returns pixelLimit as square area
|
|
305
|
+
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
|
|
306
|
+
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
|
|
307
|
+
return true;
|
|
308
|
+
default:
|
|
309
|
+
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// exit with error on ReGIS or any other requests
|
|
314
|
+
this._report(`\x1b[?${params[0]};${GaStatus.ITEM_ERROR}S`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { toRGBA8888 } from 'sixel/lib/Colors';
|
|
7
|
+
import { IDisposable } from '@sailfish-ai/xterm';
|
|
8
|
+
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
+
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
|
10
|
+
|
|
11
|
+
const PLACEHOLDER_LENGTH = 4096;
|
|
12
|
+
const PLACEHOLDER_HEIGHT = 24;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ImageRenderer - terminal frontend extension:
|
|
16
|
+
* - provide primitives for canvas, ImageData, Bitmap (static)
|
|
17
|
+
* - add canvas layer to DOM (browser only for now)
|
|
18
|
+
* - draw image tiles onRender
|
|
19
|
+
*/
|
|
20
|
+
export class ImageRenderer extends Disposable implements IDisposable {
|
|
21
|
+
public canvas: HTMLCanvasElement | undefined;
|
|
22
|
+
private _ctx: CanvasRenderingContext2D | null | undefined;
|
|
23
|
+
private _placeholder: HTMLCanvasElement | undefined;
|
|
24
|
+
private _placeholderBitmap: ImageBitmap | undefined;
|
|
25
|
+
private _optionsRefresh = this._register(new MutableDisposable());
|
|
26
|
+
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
|
27
|
+
private _renderService: IRenderService | undefined;
|
|
28
|
+
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
|
29
|
+
|
|
30
|
+
// drawing primitive - canvas
|
|
31
|
+
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
|
|
32
|
+
/**
|
|
33
|
+
* NOTE: We normally dont care, from which document the canvas
|
|
34
|
+
* gets created, so we can fall back to global document,
|
|
35
|
+
* if the terminal has no document associated yet.
|
|
36
|
+
* This way early image loads before calling .open keep working
|
|
37
|
+
* (still discouraged though, as the metrics will be screwed up).
|
|
38
|
+
* Only the DOM output canvas should be on the terminal's document,
|
|
39
|
+
* which gets explicitly checked in `insertLayerToDom`.
|
|
40
|
+
*/
|
|
41
|
+
const canvas = (localDocument || document).createElement('canvas');
|
|
42
|
+
canvas.width = width | 0;
|
|
43
|
+
canvas.height = height | 0;
|
|
44
|
+
return canvas;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// drawing primitive - ImageData with optional buffer
|
|
48
|
+
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
|
|
49
|
+
if (typeof ImageData !== 'function') {
|
|
50
|
+
const imgData = ctx.createImageData(width, height);
|
|
51
|
+
if (buffer) {
|
|
52
|
+
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
|
|
53
|
+
}
|
|
54
|
+
return imgData;
|
|
55
|
+
}
|
|
56
|
+
return buffer
|
|
57
|
+
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
|
|
58
|
+
: new ImageData(width, height);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// drawing primitive - ImageBitmap
|
|
62
|
+
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
|
|
63
|
+
if (typeof createImageBitmap !== 'function') {
|
|
64
|
+
return Promise.resolve(undefined);
|
|
65
|
+
}
|
|
66
|
+
return createImageBitmap(img);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
constructor(private _terminal: ITerminalExt) {
|
|
71
|
+
super();
|
|
72
|
+
this._oldOpen = this._terminal._core.open;
|
|
73
|
+
this._terminal._core.open = (parent: HTMLElement): void => {
|
|
74
|
+
this._oldOpen?.call(this._terminal._core, parent);
|
|
75
|
+
this._open();
|
|
76
|
+
};
|
|
77
|
+
if (this._terminal._core.screenElement) {
|
|
78
|
+
this._open();
|
|
79
|
+
}
|
|
80
|
+
// hack to spot fontSize changes
|
|
81
|
+
this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => {
|
|
82
|
+
if (option === 'fontSize') {
|
|
83
|
+
this.rescaleCanvas();
|
|
84
|
+
this._renderService?.refreshRows(0, this._terminal.rows);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
this._register(toDisposable(() => {
|
|
88
|
+
this.removeLayerFromDom();
|
|
89
|
+
if (this._terminal._core && this._oldOpen) {
|
|
90
|
+
this._terminal._core.open = this._oldOpen;
|
|
91
|
+
this._oldOpen = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (this._renderService && this._oldSetRenderer) {
|
|
94
|
+
this._renderService.setRenderer = this._oldSetRenderer;
|
|
95
|
+
this._oldSetRenderer = undefined;
|
|
96
|
+
}
|
|
97
|
+
this._renderService = undefined;
|
|
98
|
+
this.canvas = undefined;
|
|
99
|
+
this._ctx = undefined;
|
|
100
|
+
this._placeholderBitmap?.close();
|
|
101
|
+
this._placeholderBitmap = undefined;
|
|
102
|
+
this._placeholder = undefined;
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Enable the placeholder.
|
|
108
|
+
*/
|
|
109
|
+
public showPlaceholder(value: boolean): void {
|
|
110
|
+
if (value) {
|
|
111
|
+
if (!this._placeholder && this.cellSize.height !== -1) {
|
|
112
|
+
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
this._placeholderBitmap?.close();
|
|
116
|
+
this._placeholderBitmap = undefined;
|
|
117
|
+
this._placeholder = undefined;
|
|
118
|
+
}
|
|
119
|
+
this._renderService?.refreshRows(0, this._terminal.rows);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Dimensions of the terminal.
|
|
124
|
+
* Forwarded from internal render service.
|
|
125
|
+
*/
|
|
126
|
+
public get dimensions(): IRenderDimensions | undefined {
|
|
127
|
+
return this._terminal.dimensions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Current cell size (float).
|
|
132
|
+
*/
|
|
133
|
+
public get cellSize(): ICellSize {
|
|
134
|
+
return {
|
|
135
|
+
width: this.dimensions?.css.cell.width || -1,
|
|
136
|
+
height: this.dimensions?.css.cell.height || -1
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear a region of the image layer canvas.
|
|
142
|
+
*/
|
|
143
|
+
public clearLines(start: number, end: number): void {
|
|
144
|
+
this._ctx?.clearRect(
|
|
145
|
+
0,
|
|
146
|
+
start * (this.dimensions?.css.cell.height || 0),
|
|
147
|
+
this.dimensions?.css.canvas.width || 0,
|
|
148
|
+
(++end - start) * (this.dimensions?.css.cell.height || 0)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear whole image canvas.
|
|
154
|
+
*/
|
|
155
|
+
public clearAll(): void {
|
|
156
|
+
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Draw neighboring tiles on the image layer canvas.
|
|
161
|
+
*/
|
|
162
|
+
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
|
163
|
+
if (!this._ctx) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const { width, height } = this.cellSize;
|
|
167
|
+
|
|
168
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
169
|
+
if (width === -1 || height === -1) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this._rescaleImage(imgSpec, width, height);
|
|
174
|
+
const img = imgSpec.actual!;
|
|
175
|
+
const cols = Math.ceil(img.width / width);
|
|
176
|
+
|
|
177
|
+
const sx = (tileId % cols) * width;
|
|
178
|
+
const sy = Math.floor(tileId / cols) * height;
|
|
179
|
+
const dx = col * width;
|
|
180
|
+
const dy = row * height;
|
|
181
|
+
|
|
182
|
+
// safari bug: never access image source out of bounds
|
|
183
|
+
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
|
|
184
|
+
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
185
|
+
|
|
186
|
+
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
|
187
|
+
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
|
188
|
+
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
|
189
|
+
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
|
190
|
+
this._ctx.drawImage(
|
|
191
|
+
img,
|
|
192
|
+
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
|
193
|
+
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract a single tile from an image.
|
|
199
|
+
*/
|
|
200
|
+
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
|
|
201
|
+
const { width, height } = this.cellSize;
|
|
202
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
203
|
+
if (width === -1 || height === -1) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this._rescaleImage(imgSpec, width, height);
|
|
207
|
+
const img = imgSpec.actual!;
|
|
208
|
+
const cols = Math.ceil(img.width / width);
|
|
209
|
+
const sx = (tileId % cols) * width;
|
|
210
|
+
const sy = Math.floor(tileId / cols) * height;
|
|
211
|
+
const finalWidth = width + sx > img.width ? img.width - sx : width;
|
|
212
|
+
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
213
|
+
|
|
214
|
+
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
|
|
215
|
+
const ctx = canvas.getContext('2d');
|
|
216
|
+
if (ctx) {
|
|
217
|
+
ctx.drawImage(
|
|
218
|
+
img,
|
|
219
|
+
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
|
|
220
|
+
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
|
|
221
|
+
);
|
|
222
|
+
return canvas;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Draw a line with placeholder on the image layer canvas.
|
|
228
|
+
*/
|
|
229
|
+
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
|
230
|
+
if (this._ctx) {
|
|
231
|
+
const { width, height } = this.cellSize;
|
|
232
|
+
|
|
233
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
234
|
+
if (width === -1 || height === -1) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this._placeholder) {
|
|
239
|
+
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
|
|
240
|
+
} else if (height >= this._placeholder!.height) {
|
|
241
|
+
this._createPlaceHolder(height + 1);
|
|
242
|
+
}
|
|
243
|
+
if (!this._placeholder) return;
|
|
244
|
+
this._ctx.drawImage(
|
|
245
|
+
this._placeholderBitmap || this._placeholder!,
|
|
246
|
+
col * width,
|
|
247
|
+
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
248
|
+
width * count,
|
|
249
|
+
height,
|
|
250
|
+
col * width,
|
|
251
|
+
row * height,
|
|
252
|
+
width * count,
|
|
253
|
+
height
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Rescale image layer canvas if needed.
|
|
260
|
+
* Checked once from `ImageStorage.render`.
|
|
261
|
+
*/
|
|
262
|
+
public rescaleCanvas(): void {
|
|
263
|
+
if (!this.canvas) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
|
|
267
|
+
this.canvas.width = this.dimensions!.css.canvas.width || 0;
|
|
268
|
+
this.canvas.height = this.dimensions!.css.canvas.height || 0;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Rescale image in storage if needed.
|
|
274
|
+
*/
|
|
275
|
+
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
|
|
276
|
+
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
|
|
280
|
+
if (currentWidth === originalWidth && currentHeight === originalHeight) {
|
|
281
|
+
spec.actual = spec.orig;
|
|
282
|
+
spec.actualCellSize.width = originalWidth;
|
|
283
|
+
spec.actualCellSize.height = originalHeight;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const canvas = ImageRenderer.createCanvas(
|
|
287
|
+
this.document,
|
|
288
|
+
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
|
|
289
|
+
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
|
|
290
|
+
);
|
|
291
|
+
const ctx = canvas.getContext('2d');
|
|
292
|
+
if (ctx) {
|
|
293
|
+
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
|
|
294
|
+
spec.actual = canvas;
|
|
295
|
+
spec.actualCellSize.width = currentWidth;
|
|
296
|
+
spec.actualCellSize.height = currentHeight;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Lazy init for the renderer.
|
|
302
|
+
*/
|
|
303
|
+
private _open(): void {
|
|
304
|
+
this._renderService = this._terminal._core._renderService;
|
|
305
|
+
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
|
306
|
+
this._renderService.setRenderer = (renderer: any) => {
|
|
307
|
+
this.removeLayerFromDom();
|
|
308
|
+
this._oldSetRenderer?.call(this._renderService, renderer);
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public insertLayerToDom(): void {
|
|
313
|
+
// make sure that the terminal is attached to a document and to DOM
|
|
314
|
+
if (this.document && this._terminal._core.screenElement) {
|
|
315
|
+
if (!this.canvas) {
|
|
316
|
+
this.canvas = ImageRenderer.createCanvas(
|
|
317
|
+
this.document, this.dimensions?.css.canvas.width || 0,
|
|
318
|
+
this.dimensions?.css.canvas.height || 0
|
|
319
|
+
);
|
|
320
|
+
this.canvas.classList.add('xterm-image-layer');
|
|
321
|
+
this._terminal._core.screenElement.appendChild(this.canvas);
|
|
322
|
+
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
|
|
323
|
+
this.clearAll();
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
public removeLayerFromDom(): void {
|
|
331
|
+
if (this.canvas) {
|
|
332
|
+
this._ctx = undefined;
|
|
333
|
+
this.canvas.remove();
|
|
334
|
+
this.canvas = undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
|
339
|
+
this._placeholderBitmap?.close();
|
|
340
|
+
this._placeholderBitmap = undefined;
|
|
341
|
+
|
|
342
|
+
// create blueprint to fill placeholder with
|
|
343
|
+
const bWidth = 32; // must be 2^n
|
|
344
|
+
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
|
|
345
|
+
const ctx = blueprint.getContext('2d', { alpha: false });
|
|
346
|
+
if (!ctx) return;
|
|
347
|
+
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
|
|
348
|
+
const d32 = new Uint32Array(imgData.data.buffer);
|
|
349
|
+
const black = toRGBA8888(0, 0, 0);
|
|
350
|
+
const white = toRGBA8888(255, 255, 255);
|
|
351
|
+
d32.fill(black);
|
|
352
|
+
for (let y = 0; y < height; ++y) {
|
|
353
|
+
const shift = y % 2;
|
|
354
|
+
const offset = y * bWidth;
|
|
355
|
+
for (let x = 0; x < bWidth; x += 2) {
|
|
356
|
+
d32[offset + x + shift] = white;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ctx.putImageData(imgData, 0, 0);
|
|
360
|
+
|
|
361
|
+
// create placeholder line, width aligned to blueprint width
|
|
362
|
+
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
|
|
363
|
+
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
|
364
|
+
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
|
365
|
+
if (!ctx2) {
|
|
366
|
+
this._placeholder = undefined;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (let i = 0; i < width; i += bWidth) {
|
|
370
|
+
ctx2.drawImage(blueprint, i, 0);
|
|
371
|
+
}
|
|
372
|
+
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public get document(): Document | undefined {
|
|
376
|
+
return this._terminal._core._coreBrowserService?.window.document;
|
|
377
|
+
}
|
|
378
|
+
}
|