@xterm/addon-image 0.10.0-beta.28 → 0.10.0-beta.281
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/README.md +33 -42
- package/lib/addon-image.js +1 -1
- package/lib/addon-image.js.map +1 -1
- package/lib/addon-image.mjs +1 -22
- package/lib/addon-image.mjs.map +4 -4
- package/package.json +5 -5
- package/src/IIPHandler.ts +109 -41
- package/src/IIPHeaderParser.ts +89 -13
- package/src/IIPImageStorage.ts +28 -0
- package/src/IIPMetrics.ts +9 -1
- package/src/ImageAddon.ts +74 -13
- package/src/ImageRenderer.ts +99 -53
- package/src/ImageStorage.ts +138 -65
- package/src/SixelHandler.ts +4 -4
- package/src/SixelImageStorage.ts +52 -0
- package/src/Types.ts +18 -3
- package/src/kitty/KittyGraphicsHandler.ts +819 -0
- package/src/kitty/KittyGraphicsTypes.ts +195 -0
- package/src/kitty/KittyImageStorage.ts +155 -0
- package/typings/addon-image.d.ts +15 -1
package/src/ImageRenderer.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { toRGBA8888 } from 'sixel/lib/Colors';
|
|
7
7
|
import { IDisposable } from '@xterm/xterm';
|
|
8
|
-
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
-
import { Disposable, MutableDisposable, toDisposable } from '
|
|
8
|
+
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
|
9
|
+
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const enum Constants {
|
|
12
|
+
PLACEHOLDER_LENGTH = 4096,
|
|
13
|
+
PLACEHOLDER_HEIGHT = 24
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* ImageRenderer - terminal frontend extension:
|
|
@@ -18,8 +20,9 @@ const PLACEHOLDER_HEIGHT = 24;
|
|
|
18
20
|
* - draw image tiles onRender
|
|
19
21
|
*/
|
|
20
22
|
export class ImageRenderer extends Disposable implements IDisposable {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/** @deprecated Kept for backward compat — points to top layer canvas. */
|
|
24
|
+
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
|
25
|
+
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
|
23
26
|
private _placeholder: HTMLCanvasElement | undefined;
|
|
24
27
|
private _placeholderBitmap: ImageBitmap | undefined;
|
|
25
28
|
private _optionsRefresh = this._register(new MutableDisposable());
|
|
@@ -38,7 +41,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
38
41
|
* Only the DOM output canvas should be on the terminal's document,
|
|
39
42
|
* which gets explicitly checked in `insertLayerToDom`.
|
|
40
43
|
*/
|
|
41
|
-
const canvas = (localDocument
|
|
44
|
+
const canvas = (localDocument ?? document).createElement('canvas');
|
|
42
45
|
canvas.width = width | 0;
|
|
43
46
|
canvas.height = height | 0;
|
|
44
47
|
return canvas;
|
|
@@ -86,6 +89,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
86
89
|
});
|
|
87
90
|
this._register(toDisposable(() => {
|
|
88
91
|
this.removeLayerFromDom();
|
|
92
|
+
this.removeLayerFromDom('bottom');
|
|
89
93
|
if (this._terminal._core && this._oldOpen) {
|
|
90
94
|
this._terminal._core.open = this._oldOpen;
|
|
91
95
|
this._oldOpen = undefined;
|
|
@@ -95,8 +99,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
95
99
|
this._oldSetRenderer = undefined;
|
|
96
100
|
}
|
|
97
101
|
this._renderService = undefined;
|
|
98
|
-
this.
|
|
99
|
-
this._ctx = undefined;
|
|
102
|
+
this._layers.clear();
|
|
100
103
|
this._placeholderBitmap?.close();
|
|
101
104
|
this._placeholderBitmap = undefined;
|
|
102
105
|
this._placeholder = undefined;
|
|
@@ -109,7 +112,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
109
112
|
public showPlaceholder(value: boolean): void {
|
|
110
113
|
if (value) {
|
|
111
114
|
if (!this._placeholder && this.cellSize.height !== -1) {
|
|
112
|
-
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
|
|
115
|
+
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
113
116
|
}
|
|
114
117
|
} else {
|
|
115
118
|
this._placeholderBitmap?.close();
|
|
@@ -124,7 +127,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
124
127
|
* Forwarded from internal render service.
|
|
125
128
|
*/
|
|
126
129
|
public get dimensions(): IRenderDimensions | undefined {
|
|
127
|
-
return this.
|
|
130
|
+
return this._terminal.dimensions;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
/**
|
|
@@ -140,27 +143,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
140
143
|
/**
|
|
141
144
|
* Clear a region of the image layer canvas.
|
|
142
145
|
*/
|
|
143
|
-
public clearLines(start: number, end: number): void {
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(
|
|
149
|
-
|
|
146
|
+
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
|
147
|
+
const y = start * (this.dimensions?.css.cell.height || 0);
|
|
148
|
+
const w = this.dimensions?.css.canvas.width || 0;
|
|
149
|
+
const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
|
|
150
|
+
if (!layer || layer === 'top') {
|
|
151
|
+
this._layers.get('top')?.clearRect(0, y, w, h);
|
|
152
|
+
}
|
|
153
|
+
if (!layer || layer === 'bottom') {
|
|
154
|
+
this._layers.get('bottom')?.clearRect(0, y, w, h);
|
|
155
|
+
}
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
|
153
159
|
* Clear whole image canvas.
|
|
154
160
|
*/
|
|
155
|
-
public clearAll(): void {
|
|
156
|
-
|
|
161
|
+
public clearAll(layer?: ImageLayer): void {
|
|
162
|
+
if (!layer || layer === 'top') {
|
|
163
|
+
const ctx = this._layers.get('top');
|
|
164
|
+
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
165
|
+
}
|
|
166
|
+
if (!layer || layer === 'bottom') {
|
|
167
|
+
const ctx = this._layers.get('bottom');
|
|
168
|
+
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
/**
|
|
160
173
|
* Draw neighboring tiles on the image layer canvas.
|
|
161
174
|
*/
|
|
162
175
|
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
|
163
|
-
|
|
176
|
+
const ctx = this._layers.get(imgSpec.layer);
|
|
177
|
+
if (!ctx) {
|
|
164
178
|
return;
|
|
165
179
|
}
|
|
166
180
|
const { width, height } = this.cellSize;
|
|
@@ -187,7 +201,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
187
201
|
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
|
188
202
|
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
|
189
203
|
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
|
190
|
-
|
|
204
|
+
ctx.drawImage(
|
|
191
205
|
img,
|
|
192
206
|
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
|
193
207
|
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
|
@@ -227,7 +241,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
227
241
|
* Draw a line with placeholder on the image layer canvas.
|
|
228
242
|
*/
|
|
229
243
|
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
|
230
|
-
|
|
244
|
+
const ctx = this._layers.get('top');
|
|
245
|
+
if (ctx) {
|
|
231
246
|
const { width, height } = this.cellSize;
|
|
232
247
|
|
|
233
248
|
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
@@ -236,13 +251,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
if (!this._placeholder) {
|
|
239
|
-
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
|
|
254
|
+
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
240
255
|
} else if (height >= this._placeholder!.height) {
|
|
241
256
|
this._createPlaceHolder(height + 1);
|
|
242
257
|
}
|
|
243
258
|
if (!this._placeholder) return;
|
|
244
|
-
|
|
245
|
-
this._placeholderBitmap
|
|
259
|
+
ctx.drawImage(
|
|
260
|
+
this._placeholderBitmap ?? this._placeholder!,
|
|
246
261
|
col * width,
|
|
247
262
|
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
248
263
|
width * count,
|
|
@@ -260,12 +275,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
260
275
|
* Checked once from `ImageStorage.render`.
|
|
261
276
|
*/
|
|
262
277
|
public rescaleCanvas(): void {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
const w = this.dimensions?.css.canvas.width || 0;
|
|
279
|
+
const h = this.dimensions?.css.canvas.height || 0;
|
|
280
|
+
for (const ctx of this._layers.values()) {
|
|
281
|
+
if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
|
|
282
|
+
ctx.canvas.width = w;
|
|
283
|
+
ctx.canvas.height = h;
|
|
284
|
+
}
|
|
269
285
|
}
|
|
270
286
|
}
|
|
271
287
|
|
|
@@ -304,38 +320,68 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
304
320
|
this._renderService = this._terminal._core._renderService;
|
|
305
321
|
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
|
306
322
|
this._renderService.setRenderer = (renderer: any) => {
|
|
307
|
-
this.
|
|
323
|
+
for (const key of [...this._layers.keys()]) {
|
|
324
|
+
this.removeLayerFromDom(key);
|
|
325
|
+
}
|
|
308
326
|
this._oldSetRenderer?.call(this._renderService, renderer);
|
|
309
327
|
};
|
|
310
328
|
}
|
|
311
329
|
|
|
312
|
-
public insertLayerToDom(): void {
|
|
330
|
+
public insertLayerToDom(layer: ImageLayer = 'top'): void {
|
|
313
331
|
// make sure that the terminal is attached to a document and to DOM
|
|
314
|
-
if (this.document
|
|
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 {
|
|
332
|
+
if (!this.document || !this._terminal._core.screenElement) {
|
|
326
333
|
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (this._layers.has(layer)) {
|
|
337
|
+
return;
|
|
327
338
|
}
|
|
339
|
+
const canvas = ImageRenderer.createCanvas(
|
|
340
|
+
this.document, this.dimensions?.css.canvas.width || 0,
|
|
341
|
+
this.dimensions?.css.canvas.height || 0
|
|
342
|
+
);
|
|
343
|
+
canvas.classList.add(`xterm-image-layer-${layer}`);
|
|
344
|
+
const screenElement = this._terminal._core.screenElement;
|
|
345
|
+
// Use isolation to create a stacking context without overriding z-index,
|
|
346
|
+
// which would conflict with integrators (e.g. VS Code) that set their
|
|
347
|
+
// own z-index on the screen element.
|
|
348
|
+
screenElement.style.isolation = 'isolate';
|
|
349
|
+
if (layer === 'bottom') {
|
|
350
|
+
// Use z-index:-1 so it paints behind non-positioned text elements.
|
|
351
|
+
// The screen element needs to be a stacking context (via isolation)
|
|
352
|
+
// to contain the negative z-index, otherwise it would go behind the
|
|
353
|
+
// entire terminal.
|
|
354
|
+
canvas.style.zIndex = '-1';
|
|
355
|
+
screenElement.insertBefore(canvas, screenElement.firstChild);
|
|
356
|
+
} else {
|
|
357
|
+
// Explicit z-index ensures the image canvas reliably stacks above
|
|
358
|
+
// the text layer (DOM renderer rows). z-index: 0 is below the
|
|
359
|
+
// selection overlay (z-index: 1).
|
|
360
|
+
canvas.style.zIndex = '0';
|
|
361
|
+
screenElement.appendChild(canvas);
|
|
362
|
+
}
|
|
363
|
+
const ctx = canvas.getContext('2d', { alpha: true });
|
|
364
|
+
if (!ctx) {
|
|
365
|
+
canvas.remove();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this._layers.set(layer, ctx);
|
|
369
|
+
this.clearAll(layer);
|
|
328
370
|
}
|
|
329
371
|
|
|
330
|
-
public removeLayerFromDom(): void {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
372
|
+
public removeLayerFromDom(layer: ImageLayer = 'top'): void {
|
|
373
|
+
const ctx = this._layers.get(layer);
|
|
374
|
+
if (ctx) {
|
|
375
|
+
ctx.canvas.remove();
|
|
376
|
+
this._layers.delete(layer);
|
|
335
377
|
}
|
|
336
378
|
}
|
|
337
379
|
|
|
338
|
-
|
|
380
|
+
public hasLayer(layer: ImageLayer): boolean {
|
|
381
|
+
return this._layers.has(layer);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
|
|
339
385
|
this._placeholderBitmap?.close();
|
|
340
386
|
this._placeholderBitmap = undefined;
|
|
341
387
|
|
|
@@ -359,7 +405,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|
|
359
405
|
ctx.putImageData(imgData, 0, 0);
|
|
360
406
|
|
|
361
407
|
// create placeholder line, width aligned to blueprint width
|
|
362
|
-
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
|
|
408
|
+
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
|
|
363
409
|
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
|
364
410
|
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
|
365
411
|
if (!ctx2) {
|
package/src/ImageStorage.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { IDisposable } from '@xterm/xterm';
|
|
7
7
|
import { ImageRenderer } from './ImageRenderer';
|
|
8
|
-
import {
|
|
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
|
|
@@ -124,6 +128,8 @@ export class ImageStorage implements IDisposable {
|
|
|
124
128
|
private _pixelLimit: number = 2500000;
|
|
125
129
|
|
|
126
130
|
private _viewportMetrics: { cols: number, rows: number };
|
|
131
|
+
public onImageAdded: (() => void) | undefined;
|
|
132
|
+
public onImageDeleted: ((storageId: number) => void) | undefined;
|
|
127
133
|
|
|
128
134
|
constructor(
|
|
129
135
|
private _terminal: ITerminalExt,
|
|
@@ -132,8 +138,10 @@ export class ImageStorage implements IDisposable {
|
|
|
132
138
|
) {
|
|
133
139
|
try {
|
|
134
140
|
this.setLimit(this._opts.storageLimit);
|
|
135
|
-
} catch (e:
|
|
136
|
-
|
|
141
|
+
} catch (e: unknown) {
|
|
142
|
+
if (e instanceof Error) {
|
|
143
|
+
console.error(e.message);
|
|
144
|
+
}
|
|
137
145
|
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
|
138
146
|
}
|
|
139
147
|
this._viewportMetrics = {
|
|
@@ -187,11 +195,13 @@ export class ImageStorage implements IDisposable {
|
|
|
187
195
|
|
|
188
196
|
private _delImg(id: number): void {
|
|
189
197
|
const spec = this._images.get(id);
|
|
198
|
+
if (!spec) return;
|
|
190
199
|
this._images.delete(id);
|
|
191
200
|
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
|
192
|
-
if (
|
|
201
|
+
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
|
193
202
|
spec.orig.close();
|
|
194
203
|
}
|
|
204
|
+
this.onImageDeleted?.(id);
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
/**
|
|
@@ -215,27 +225,29 @@ export class ImageStorage implements IDisposable {
|
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
|
228
|
+
* Delete an image by its internal storage ID.
|
|
229
|
+
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
|
221
230
|
*/
|
|
222
|
-
public
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
const rows = Math.ceil(height / cellSize.height);
|
|
229
|
-
for (let i = 1; i < rows; ++i) {
|
|
230
|
-
this._terminal._core._inputHandler.lineFeed();
|
|
231
|
-
}
|
|
231
|
+
public deleteImage(id: number): void {
|
|
232
|
+
const spec = this._images.get(id);
|
|
233
|
+
if (spec) {
|
|
234
|
+
spec.marker?.dispose();
|
|
235
|
+
this._delImg(id);
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
/**
|
|
236
240
|
* Method to add an image to the storage.
|
|
241
|
+
* @param img - The image to add (canvas or bitmap).
|
|
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.
|
|
248
|
+
* @returns The internal image ID assigned to the stored image.
|
|
237
249
|
*/
|
|
238
|
-
public addImage(img: HTMLCanvasElement | ImageBitmap):
|
|
250
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
|
|
239
251
|
// never allow storage to exceed memory limit
|
|
240
252
|
this._evictOldest(img.width * img.height);
|
|
241
253
|
|
|
@@ -257,7 +269,7 @@ export class ImageStorage implements IDisposable {
|
|
|
257
269
|
let offset = originX;
|
|
258
270
|
let tileCount = 0;
|
|
259
271
|
|
|
260
|
-
if (!
|
|
272
|
+
if (!opts.scrolling) {
|
|
261
273
|
buffer.x = 0;
|
|
262
274
|
buffer.y = 0;
|
|
263
275
|
offset = 0;
|
|
@@ -271,7 +283,7 @@ export class ImageStorage implements IDisposable {
|
|
|
271
283
|
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
|
272
284
|
tileCount++;
|
|
273
285
|
}
|
|
274
|
-
if (
|
|
286
|
+
if (opts.scrolling) {
|
|
275
287
|
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
|
276
288
|
} else {
|
|
277
289
|
if (++buffer.y >= termRows) break;
|
|
@@ -281,8 +293,12 @@ export class ImageStorage implements IDisposable {
|
|
|
281
293
|
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
|
282
294
|
|
|
283
295
|
// cursor positioning modes
|
|
284
|
-
if (
|
|
285
|
-
|
|
296
|
+
if (opts.scrolling) {
|
|
297
|
+
if (opts.cursorPos === 'iip') {
|
|
298
|
+
buffer.x = Math.min(offset + cols, termCols);
|
|
299
|
+
} else {
|
|
300
|
+
buffer.x = offset;
|
|
301
|
+
}
|
|
286
302
|
} else {
|
|
287
303
|
buffer.x = originX;
|
|
288
304
|
buffer.y = originY;
|
|
@@ -324,11 +340,15 @@ export class ImageStorage implements IDisposable {
|
|
|
324
340
|
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
|
325
341
|
marker: endMarker || undefined,
|
|
326
342
|
tileCount,
|
|
327
|
-
bufferType: this._terminal.buffer.active.type
|
|
343
|
+
bufferType: this._terminal.buffer.active.type,
|
|
344
|
+
layer: opts.layer,
|
|
345
|
+
zIndex: opts.zIndex
|
|
328
346
|
};
|
|
329
347
|
|
|
330
348
|
// finally add the image
|
|
331
349
|
this._images.set(imageId, imgSpec);
|
|
350
|
+
this.onImageAdded?.();
|
|
351
|
+
return imageId;
|
|
332
352
|
}
|
|
333
353
|
|
|
334
354
|
|
|
@@ -338,16 +358,30 @@ export class ImageStorage implements IDisposable {
|
|
|
338
358
|
*/
|
|
339
359
|
// TODO: Should we move this to the ImageRenderer?
|
|
340
360
|
public render(range: { start: number, end: number }): void {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
361
|
+
// Determine which layers have images
|
|
362
|
+
let hasTopImages = false;
|
|
363
|
+
let hasBottomImages = false;
|
|
364
|
+
for (const spec of this._images.values()) {
|
|
365
|
+
if (spec.layer === 'bottom') {
|
|
366
|
+
hasBottomImages = true;
|
|
367
|
+
} else {
|
|
368
|
+
hasTopImages = true;
|
|
347
369
|
}
|
|
370
|
+
if (hasTopImages && hasBottomImages) break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Lazily insert layers that are needed
|
|
374
|
+
if (hasTopImages && !this._renderer.hasLayer('top')) {
|
|
375
|
+
this._renderer.insertLayerToDom('top');
|
|
376
|
+
if (!this._renderer.hasLayer('top')) return;
|
|
377
|
+
}
|
|
378
|
+
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
|
|
379
|
+
this._renderer.insertLayerToDom('bottom');
|
|
348
380
|
}
|
|
381
|
+
|
|
349
382
|
// rescale if needed
|
|
350
383
|
this._renderer.rescaleCanvas();
|
|
384
|
+
|
|
351
385
|
// exit early if we dont have any images to test for
|
|
352
386
|
if (!this._images.size) {
|
|
353
387
|
if (!this._fullyCleared) {
|
|
@@ -355,12 +389,25 @@ export class ImageStorage implements IDisposable {
|
|
|
355
389
|
this._fullyCleared = true;
|
|
356
390
|
this._needsFullClear = false;
|
|
357
391
|
}
|
|
358
|
-
if (this._renderer.
|
|
359
|
-
this._renderer.removeLayerFromDom();
|
|
392
|
+
if (this._renderer.hasLayer('top')) {
|
|
393
|
+
this._renderer.removeLayerFromDom('top');
|
|
394
|
+
}
|
|
395
|
+
if (this._renderer.hasLayer('bottom')) {
|
|
396
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
360
397
|
}
|
|
361
398
|
return;
|
|
362
399
|
}
|
|
363
400
|
|
|
401
|
+
// Remove layers no longer needed
|
|
402
|
+
if (!hasTopImages && this._renderer.hasLayer('top')) {
|
|
403
|
+
this._renderer.clearAll('top');
|
|
404
|
+
this._renderer.removeLayerFromDom('top');
|
|
405
|
+
}
|
|
406
|
+
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
|
|
407
|
+
this._renderer.clearAll('bottom');
|
|
408
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
409
|
+
}
|
|
410
|
+
|
|
364
411
|
// buffer switches force a full clear
|
|
365
412
|
if (this._needsFullClear) {
|
|
366
413
|
this._renderer.clearAll();
|
|
@@ -375,50 +422,76 @@ export class ImageStorage implements IDisposable {
|
|
|
375
422
|
// clear drawing area
|
|
376
423
|
this._renderer.clearLines(start, end);
|
|
377
424
|
|
|
378
|
-
//
|
|
425
|
+
// Collect draw calls so we can sort by z-index (lower z drawn first).
|
|
426
|
+
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
|
|
427
|
+
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
|
428
|
+
|
|
429
|
+
// walk all cells in viewport and collect tiles found
|
|
430
|
+
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
|
|
431
|
+
// because text writes clear the BG flag but leave image tile data intact.
|
|
432
|
+
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
|
|
379
433
|
for (let row = start; row <= end; ++row) {
|
|
380
434
|
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
|
381
435
|
if (!line) return;
|
|
382
436
|
for (let col = 0; col < cols; ++col) {
|
|
437
|
+
let e: IExtendedAttrsImage;
|
|
383
438
|
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
439
|
+
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
|
440
|
+
} else {
|
|
441
|
+
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
442
|
+
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
|
387
443
|
continue;
|
|
388
444
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
445
|
+
e = maybeImg;
|
|
446
|
+
}
|
|
447
|
+
const imageId = e.imageId;
|
|
448
|
+
if (imageId === undefined || imageId === -1) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const imgSpec = this._images.get(imageId);
|
|
452
|
+
if (e.tileId !== -1) {
|
|
453
|
+
const startTile = e.tileId;
|
|
454
|
+
const startCol = col;
|
|
455
|
+
let count = 1;
|
|
456
|
+
/**
|
|
457
|
+
* merge tiles to the right into a single draw call, if:
|
|
458
|
+
* - not at end of line
|
|
459
|
+
* - cell has same image id
|
|
460
|
+
* - cell has consecutive tile id
|
|
461
|
+
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
|
462
|
+
*/
|
|
463
|
+
while (++col < cols) {
|
|
464
|
+
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
465
|
+
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
|
466
|
+
break;
|
|
408
467
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this._renderer.drawPlaceholder(startCol, row, count);
|
|
468
|
+
count++;
|
|
469
|
+
}
|
|
470
|
+
col--;
|
|
471
|
+
if (imgSpec) {
|
|
472
|
+
if (imgSpec.actual) {
|
|
473
|
+
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
|
416
474
|
}
|
|
417
|
-
|
|
475
|
+
} else if (this._opts.showPlaceholder) {
|
|
476
|
+
placeholderCalls.push({ col: startCol, row, count });
|
|
418
477
|
}
|
|
478
|
+
this._fullyCleared = false;
|
|
419
479
|
}
|
|
420
480
|
}
|
|
421
481
|
}
|
|
482
|
+
|
|
483
|
+
// Sort by z-index so lower z draws first (higher z renders on top)
|
|
484
|
+
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
|
|
485
|
+
|
|
486
|
+
// Draw placeholders first (lowest priority)
|
|
487
|
+
for (const call of placeholderCalls) {
|
|
488
|
+
this._renderer.drawPlaceholder(call.col, call.row, call.count);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Draw images in z-index order
|
|
492
|
+
for (const call of drawCalls) {
|
|
493
|
+
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
|
|
494
|
+
}
|
|
422
495
|
}
|
|
423
496
|
|
|
424
497
|
public viewportResize(metrics: { cols: number, rows: number }): void {
|
|
@@ -442,7 +515,7 @@ export class ImageStorage implements IDisposable {
|
|
|
442
515
|
for (let row = 0; row < rows; ++row) {
|
|
443
516
|
const line = buffer.lines.get(row) as IBufferLineExt;
|
|
444
517
|
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
|
445
|
-
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol]
|
|
518
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
|
446
519
|
const imageId = e.imageId;
|
|
447
520
|
if (imageId === undefined || imageId === -1) {
|
|
448
521
|
continue;
|
|
@@ -487,7 +560,7 @@ export class ImageStorage implements IDisposable {
|
|
|
487
560
|
const buffer = this._terminal._core.buffer;
|
|
488
561
|
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
489
562
|
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
|
490
|
-
const e: IExtendedAttrsImage = line._extendedAttrs[x]
|
|
563
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
|
491
564
|
if (e.imageId && e.imageId !== -1) {
|
|
492
565
|
const orig = this._images.get(e.imageId)?.orig;
|
|
493
566
|
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
|
@@ -507,7 +580,7 @@ export class ImageStorage implements IDisposable {
|
|
|
507
580
|
const buffer = this._terminal._core.buffer;
|
|
508
581
|
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
509
582
|
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
|
510
|
-
const e: IExtendedAttrsImage = line._extendedAttrs[x]
|
|
583
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
|
511
584
|
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
512
585
|
const spec = this._images.get(e.imageId);
|
|
513
586
|
if (spec) {
|
package/src/SixelHandler.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @license MIT
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { SixelImageStorage } from './SixelImageStorage';
|
|
7
7
|
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
|
8
8
|
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
|
9
9
|
import { RGBA8888 } from 'sixel/lib/Types';
|
|
@@ -26,7 +26,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
|
|
26
26
|
|
|
27
27
|
constructor(
|
|
28
28
|
private readonly _opts: IImageAddonOptions,
|
|
29
|
-
private readonly _storage:
|
|
29
|
+
private readonly _storage: SixelImageStorage,
|
|
30
30
|
private readonly _coreTerminal: ITerminalExt
|
|
31
31
|
) {
|
|
32
32
|
DecoderAsync({
|
|
@@ -91,7 +91,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
|
|
91
91
|
const height = this._dec.height;
|
|
92
92
|
|
|
93
93
|
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
|
|
94
|
-
if (!width || !
|
|
94
|
+
if (!width || !height) {
|
|
95
95
|
if (height) {
|
|
96
96
|
this._storage.advanceCursor(height);
|
|
97
97
|
}
|
|
@@ -99,7 +99,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const canvas = ImageRenderer.createCanvas(undefined, width, height);
|
|
102
|
-
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8
|
|
102
|
+
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
|
103
103
|
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
|
104
104
|
this._dec.release();
|
|
105
105
|
}
|