@xterm/addon-image 0.7.0-beta.1
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.LICENSE.txt +24 -0
- package/lib/addon-image.js.map +1 -0
- package/out/IIPHandler.js +148 -0
- package/out/IIPHandler.js.map +1 -0
- package/out/IIPHeaderParser.js +156 -0
- package/out/IIPHeaderParser.js.map +1 -0
- package/out/IIPHeaderParser.test.js +137 -0
- package/out/IIPHeaderParser.test.js.map +1 -0
- package/out/IIPMetrics.js +71 -0
- package/out/IIPMetrics.js.map +1 -0
- package/out/IIPMetrics.test.js +38 -0
- package/out/IIPMetrics.test.js.map +1 -0
- package/out/ImageAddon.js +261 -0
- package/out/ImageAddon.js.map +1 -0
- package/out/ImageRenderer.js +330 -0
- package/out/ImageRenderer.js.map +1 -0
- package/out/ImageStorage.js +552 -0
- package/out/ImageStorage.js.map +1 -0
- package/out/SixelHandler.js +140 -0
- package/out/SixelHandler.js.map +1 -0
- package/package.json +31 -0
- package/src/IIPHandler.ts +161 -0
- package/src/IIPHeaderParser.test.ts +138 -0
- package/src/IIPHeaderParser.ts +186 -0
- package/src/IIPMetrics.test.ts +43 -0
- package/src/IIPMetrics.ts +81 -0
- package/src/ImageAddon.ts +317 -0
- package/src/ImageRenderer.ts +379 -0
- package/src/ImageStorage.ts +592 -0
- package/src/SixelHandler.ts +151 -0
- package/src/Types.d.ts +108 -0
- package/typings/addon-image.d.ts +120 -0
|
@@ -0,0 +1,379 @@
|
|
|
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 '@xterm/xterm';
|
|
8
|
+
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
+
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const PLACEHOLDER_LENGTH = 4096;
|
|
13
|
+
const PLACEHOLDER_HEIGHT = 24;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ImageRenderer - terminal frontend extension:
|
|
17
|
+
* - provide primitives for canvas, ImageData, Bitmap (static)
|
|
18
|
+
* - add canvas layer to DOM (browser only for now)
|
|
19
|
+
* - draw image tiles onRender
|
|
20
|
+
*/
|
|
21
|
+
export class ImageRenderer extends Disposable implements IDisposable {
|
|
22
|
+
public canvas: HTMLCanvasElement | undefined;
|
|
23
|
+
private _ctx: CanvasRenderingContext2D | null | undefined;
|
|
24
|
+
private _placeholder: HTMLCanvasElement | undefined;
|
|
25
|
+
private _placeholderBitmap: ImageBitmap | undefined;
|
|
26
|
+
private _optionsRefresh = this.register(new MutableDisposable());
|
|
27
|
+
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
|
28
|
+
private _renderService: IRenderService | undefined;
|
|
29
|
+
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
|
30
|
+
|
|
31
|
+
// drawing primitive - canvas
|
|
32
|
+
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
|
|
33
|
+
/**
|
|
34
|
+
* NOTE: We normally dont care, from which document the canvas
|
|
35
|
+
* gets created, so we can fall back to global document,
|
|
36
|
+
* if the terminal has no document associated yet.
|
|
37
|
+
* This way early image loads before calling .open keep working
|
|
38
|
+
* (still discouraged though, as the metrics will be screwed up).
|
|
39
|
+
* Only the DOM output canvas should be on the terminal's document,
|
|
40
|
+
* which gets explicitly checked in `insertLayerToDom`.
|
|
41
|
+
*/
|
|
42
|
+
const canvas = (localDocument || document).createElement('canvas');
|
|
43
|
+
canvas.width = width | 0;
|
|
44
|
+
canvas.height = height | 0;
|
|
45
|
+
return canvas;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// drawing primitive - ImageData with optional buffer
|
|
49
|
+
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
|
|
50
|
+
if (typeof ImageData !== 'function') {
|
|
51
|
+
const imgData = ctx.createImageData(width, height);
|
|
52
|
+
if (buffer) {
|
|
53
|
+
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
|
|
54
|
+
}
|
|
55
|
+
return imgData;
|
|
56
|
+
}
|
|
57
|
+
return buffer
|
|
58
|
+
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
|
|
59
|
+
: new ImageData(width, height);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// drawing primitive - ImageBitmap
|
|
63
|
+
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
|
|
64
|
+
if (typeof createImageBitmap !== 'function') {
|
|
65
|
+
return Promise.resolve(undefined);
|
|
66
|
+
}
|
|
67
|
+
return createImageBitmap(img);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
constructor(private _terminal: ITerminalExt) {
|
|
72
|
+
super();
|
|
73
|
+
this._oldOpen = this._terminal._core.open;
|
|
74
|
+
this._terminal._core.open = (parent: HTMLElement): void => {
|
|
75
|
+
this._oldOpen?.call(this._terminal._core, parent);
|
|
76
|
+
this._open();
|
|
77
|
+
};
|
|
78
|
+
if (this._terminal._core.screenElement) {
|
|
79
|
+
this._open();
|
|
80
|
+
}
|
|
81
|
+
// hack to spot fontSize changes
|
|
82
|
+
this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => {
|
|
83
|
+
if (option === 'fontSize') {
|
|
84
|
+
this.rescaleCanvas();
|
|
85
|
+
this._renderService?.refreshRows(0, this._terminal.rows);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.register(toDisposable(() => {
|
|
89
|
+
this.removeLayerFromDom();
|
|
90
|
+
if (this._terminal._core && this._oldOpen) {
|
|
91
|
+
this._terminal._core.open = this._oldOpen;
|
|
92
|
+
this._oldOpen = undefined;
|
|
93
|
+
}
|
|
94
|
+
if (this._renderService && this._oldSetRenderer) {
|
|
95
|
+
this._renderService.setRenderer = this._oldSetRenderer;
|
|
96
|
+
this._oldSetRenderer = undefined;
|
|
97
|
+
}
|
|
98
|
+
this._renderService = undefined;
|
|
99
|
+
this.canvas = undefined;
|
|
100
|
+
this._ctx = undefined;
|
|
101
|
+
this._placeholderBitmap?.close();
|
|
102
|
+
this._placeholderBitmap = undefined;
|
|
103
|
+
this._placeholder = undefined;
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Enable the placeholder.
|
|
109
|
+
*/
|
|
110
|
+
public showPlaceholder(value: boolean): void {
|
|
111
|
+
if (value) {
|
|
112
|
+
if (!this._placeholder && this.cellSize.height !== -1) {
|
|
113
|
+
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this._placeholderBitmap?.close();
|
|
117
|
+
this._placeholderBitmap = undefined;
|
|
118
|
+
this._placeholder = undefined;
|
|
119
|
+
}
|
|
120
|
+
this._renderService?.refreshRows(0, this._terminal.rows);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Dimensions of the terminal.
|
|
125
|
+
* Forwarded from internal render service.
|
|
126
|
+
*/
|
|
127
|
+
public get dimensions(): IRenderDimensions | undefined {
|
|
128
|
+
return this._renderService?.dimensions;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Current cell size (float).
|
|
133
|
+
*/
|
|
134
|
+
public get cellSize(): ICellSize {
|
|
135
|
+
return {
|
|
136
|
+
width: this.dimensions?.css.cell.width || -1,
|
|
137
|
+
height: this.dimensions?.css.cell.height || -1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clear a region of the image layer canvas.
|
|
143
|
+
*/
|
|
144
|
+
public clearLines(start: number, end: number): void {
|
|
145
|
+
this._ctx?.clearRect(
|
|
146
|
+
0,
|
|
147
|
+
start * (this.dimensions?.css.cell.height || 0),
|
|
148
|
+
this.dimensions?.css.canvas.width || 0,
|
|
149
|
+
(++end - start) * (this.dimensions?.css.cell.height || 0)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Clear whole image canvas.
|
|
155
|
+
*/
|
|
156
|
+
public clearAll(): void {
|
|
157
|
+
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Draw neighboring tiles on the image layer canvas.
|
|
162
|
+
*/
|
|
163
|
+
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
|
164
|
+
if (!this._ctx) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const { width, height } = this.cellSize;
|
|
168
|
+
|
|
169
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
170
|
+
if (width === -1 || height === -1) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._rescaleImage(imgSpec, width, height);
|
|
175
|
+
const img = imgSpec.actual!;
|
|
176
|
+
const cols = Math.ceil(img.width / width);
|
|
177
|
+
|
|
178
|
+
const sx = (tileId % cols) * width;
|
|
179
|
+
const sy = Math.floor(tileId / cols) * height;
|
|
180
|
+
const dx = col * width;
|
|
181
|
+
const dy = row * height;
|
|
182
|
+
|
|
183
|
+
// safari bug: never access image source out of bounds
|
|
184
|
+
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
|
|
185
|
+
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
186
|
+
|
|
187
|
+
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
|
188
|
+
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
|
189
|
+
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
|
190
|
+
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
|
191
|
+
this._ctx.drawImage(
|
|
192
|
+
img,
|
|
193
|
+
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
|
194
|
+
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extract a single tile from an image.
|
|
200
|
+
*/
|
|
201
|
+
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
|
|
202
|
+
const { width, height } = this.cellSize;
|
|
203
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
204
|
+
if (width === -1 || height === -1) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this._rescaleImage(imgSpec, width, height);
|
|
208
|
+
const img = imgSpec.actual!;
|
|
209
|
+
const cols = Math.ceil(img.width / width);
|
|
210
|
+
const sx = (tileId % cols) * width;
|
|
211
|
+
const sy = Math.floor(tileId / cols) * height;
|
|
212
|
+
const finalWidth = width + sx > img.width ? img.width - sx : width;
|
|
213
|
+
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
214
|
+
|
|
215
|
+
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
|
|
216
|
+
const ctx = canvas.getContext('2d');
|
|
217
|
+
if (ctx) {
|
|
218
|
+
ctx.drawImage(
|
|
219
|
+
img,
|
|
220
|
+
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
|
|
221
|
+
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
|
|
222
|
+
);
|
|
223
|
+
return canvas;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Draw a line with placeholder on the image layer canvas.
|
|
229
|
+
*/
|
|
230
|
+
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
|
231
|
+
if (this._ctx) {
|
|
232
|
+
const { width, height } = this.cellSize;
|
|
233
|
+
|
|
234
|
+
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
235
|
+
if (width === -1 || height === -1) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!this._placeholder) {
|
|
240
|
+
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
|
|
241
|
+
} else if (height >= this._placeholder!.height) {
|
|
242
|
+
this._createPlaceHolder(height + 1);
|
|
243
|
+
}
|
|
244
|
+
if (!this._placeholder) return;
|
|
245
|
+
this._ctx.drawImage(
|
|
246
|
+
this._placeholderBitmap || this._placeholder!,
|
|
247
|
+
col * width,
|
|
248
|
+
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
249
|
+
width * count,
|
|
250
|
+
height,
|
|
251
|
+
col * width,
|
|
252
|
+
row * height,
|
|
253
|
+
width * count,
|
|
254
|
+
height
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Rescale image layer canvas if needed.
|
|
261
|
+
* Checked once from `ImageStorage.render`.
|
|
262
|
+
*/
|
|
263
|
+
public rescaleCanvas(): void {
|
|
264
|
+
if (!this.canvas) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
|
|
268
|
+
this.canvas.width = this.dimensions!.css.canvas.width || 0;
|
|
269
|
+
this.canvas.height = this.dimensions!.css.canvas.height || 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Rescale image in storage if needed.
|
|
275
|
+
*/
|
|
276
|
+
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
|
|
277
|
+
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
|
|
281
|
+
if (currentWidth === originalWidth && currentHeight === originalHeight) {
|
|
282
|
+
spec.actual = spec.orig;
|
|
283
|
+
spec.actualCellSize.width = originalWidth;
|
|
284
|
+
spec.actualCellSize.height = originalHeight;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const canvas = ImageRenderer.createCanvas(
|
|
288
|
+
this.document,
|
|
289
|
+
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
|
|
290
|
+
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
|
|
291
|
+
);
|
|
292
|
+
const ctx = canvas.getContext('2d');
|
|
293
|
+
if (ctx) {
|
|
294
|
+
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
|
|
295
|
+
spec.actual = canvas;
|
|
296
|
+
spec.actualCellSize.width = currentWidth;
|
|
297
|
+
spec.actualCellSize.height = currentHeight;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Lazy init for the renderer.
|
|
303
|
+
*/
|
|
304
|
+
private _open(): void {
|
|
305
|
+
this._renderService = this._terminal._core._renderService;
|
|
306
|
+
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
|
307
|
+
this._renderService.setRenderer = (renderer: any) => {
|
|
308
|
+
this.removeLayerFromDom();
|
|
309
|
+
this._oldSetRenderer?.call(this._renderService, renderer);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public insertLayerToDom(): void {
|
|
314
|
+
// make sure that the terminal is attached to a document and to DOM
|
|
315
|
+
if (this.document && this._terminal._core.screenElement) {
|
|
316
|
+
if (!this.canvas) {
|
|
317
|
+
this.canvas = ImageRenderer.createCanvas(
|
|
318
|
+
this.document, this.dimensions?.css.canvas.width || 0,
|
|
319
|
+
this.dimensions?.css.canvas.height || 0
|
|
320
|
+
);
|
|
321
|
+
this.canvas.classList.add('xterm-image-layer');
|
|
322
|
+
this._terminal._core.screenElement.appendChild(this.canvas);
|
|
323
|
+
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
|
|
324
|
+
this.clearAll();
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public removeLayerFromDom(): void {
|
|
332
|
+
if (this.canvas) {
|
|
333
|
+
this._ctx = undefined;
|
|
334
|
+
this.canvas.remove();
|
|
335
|
+
this.canvas = undefined;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
|
340
|
+
this._placeholderBitmap?.close();
|
|
341
|
+
this._placeholderBitmap = undefined;
|
|
342
|
+
|
|
343
|
+
// create blueprint to fill placeholder with
|
|
344
|
+
const bWidth = 32; // must be 2^n
|
|
345
|
+
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
|
|
346
|
+
const ctx = blueprint.getContext('2d', { alpha: false });
|
|
347
|
+
if (!ctx) return;
|
|
348
|
+
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
|
|
349
|
+
const d32 = new Uint32Array(imgData.data.buffer);
|
|
350
|
+
const black = toRGBA8888(0, 0, 0);
|
|
351
|
+
const white = toRGBA8888(255, 255, 255);
|
|
352
|
+
d32.fill(black);
|
|
353
|
+
for (let y = 0; y < height; ++y) {
|
|
354
|
+
const shift = y % 2;
|
|
355
|
+
const offset = y * bWidth;
|
|
356
|
+
for (let x = 0; x < bWidth; x += 2) {
|
|
357
|
+
d32[offset + x + shift] = white;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
ctx.putImageData(imgData, 0, 0);
|
|
361
|
+
|
|
362
|
+
// create placeholder line, width aligned to blueprint width
|
|
363
|
+
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
|
|
364
|
+
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
|
365
|
+
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
|
366
|
+
if (!ctx2) {
|
|
367
|
+
this._placeholder = undefined;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
for (let i = 0; i < width; i += bWidth) {
|
|
371
|
+
ctx2.drawImage(blueprint, i, 0);
|
|
372
|
+
}
|
|
373
|
+
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public get document(): Document | undefined {
|
|
377
|
+
return this._terminal._core._coreBrowserService?.window.document;
|
|
378
|
+
}
|
|
379
|
+
}
|