@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.
@@ -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
+ }