@xterm/addon-image 0.10.0-beta.31 → 0.10.0-beta.310
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 +40 -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 +124 -47
- package/src/IIPHeaderParser.ts +89 -13
- package/src/IIPImageStorage.ts +28 -0
- package/src/IIPMetrics.ts +71 -5
- package/src/ImageAddon.ts +74 -13
- package/src/ImageRenderer.ts +99 -53
- package/src/ImageStorage.ts +151 -172
- package/src/SixelHandler.ts +4 -4
- package/src/SixelImageStorage.ts +52 -0
- package/src/Types.ts +19 -18
- 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/ImageStorage.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { IDisposable } from '@xterm/xterm';
|
|
7
7
|
import { ImageRenderer } from './ImageRenderer';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import type {
|
|
9
|
+
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts
|
|
10
|
+
} from './Types';
|
|
11
|
+
import type { IBufferLine } from 'common/buffer/Types';
|
|
12
|
+
import { CellData } from 'common/buffer/CellData';
|
|
10
13
|
|
|
11
14
|
// fallback default cell size
|
|
12
15
|
export const CELL_SIZE_DEFAULT: ICellSize = {
|
|
@@ -14,93 +17,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
|
|
|
14
17
|
height: 14
|
|
15
18
|
};
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
* Extend extended attribute to also hold image tile information.
|
|
19
|
-
*
|
|
20
|
-
* Object definition is copied from base repo to fully mimick its behavior.
|
|
21
|
-
* Image data is added as additional public properties `imageId` and `tileId`.
|
|
22
|
-
*/
|
|
23
|
-
class ExtendedAttrsImage implements IExtendedAttrsImage {
|
|
24
|
-
private _ext: number = 0;
|
|
25
|
-
public get ext(): number {
|
|
26
|
-
if (this._urlId) {
|
|
27
|
-
return (
|
|
28
|
-
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
|
|
29
|
-
(this.underlineStyle << 26)
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return this._ext;
|
|
33
|
-
}
|
|
34
|
-
public set ext(value: number) { this._ext = value; }
|
|
35
|
-
|
|
36
|
-
public get underlineStyle(): UnderlineStyle {
|
|
37
|
-
// Always return the URL style if it has one
|
|
38
|
-
if (this._urlId) {
|
|
39
|
-
return UnderlineStyle.DASHED;
|
|
40
|
-
}
|
|
41
|
-
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
|
|
42
|
-
}
|
|
43
|
-
public set underlineStyle(value: UnderlineStyle) {
|
|
44
|
-
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
|
|
45
|
-
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public get underlineColor(): number {
|
|
49
|
-
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
50
|
-
}
|
|
51
|
-
public set underlineColor(value: number) {
|
|
52
|
-
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
53
|
-
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public get underlineVariantOffset(): number {
|
|
57
|
-
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
|
|
58
|
-
if (val < 0) {
|
|
59
|
-
return val ^ 0xFFFFFFF8;
|
|
60
|
-
}
|
|
61
|
-
return val;
|
|
62
|
-
}
|
|
63
|
-
public set underlineVariantOffset(value: number) {
|
|
64
|
-
this._ext &= ~ExtFlags.VARIANT_OFFSET;
|
|
65
|
-
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private _urlId: number = 0;
|
|
69
|
-
public get urlId(): number {
|
|
70
|
-
return this._urlId;
|
|
71
|
-
}
|
|
72
|
-
public set urlId(value: number) {
|
|
73
|
-
this._urlId = value;
|
|
74
|
-
}
|
|
75
|
-
|
|
20
|
+
class ImageTileInfo {
|
|
76
21
|
constructor(
|
|
77
|
-
ext: number = 0,
|
|
78
|
-
urlId: number = 0,
|
|
79
22
|
public imageId = -1,
|
|
80
|
-
public tileId = -1
|
|
81
|
-
) {
|
|
82
|
-
this._ext = ext;
|
|
83
|
-
this._urlId = urlId;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public clone(): IExtendedAttrsImage {
|
|
87
|
-
/**
|
|
88
|
-
* Technically we dont need a clone variant of ExtendedAttrsImage,
|
|
89
|
-
* as we never clone a cell holding image data.
|
|
90
|
-
* Note: Clone is only meant to be used by the InputHandler for
|
|
91
|
-
* sticky attributes, which is never the case for image data.
|
|
92
|
-
* We still provide a proper clone method to reflect the full ext attr
|
|
93
|
-
* state in case there are future use cases for clone.
|
|
94
|
-
*/
|
|
95
|
-
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
public isEmpty(): boolean {
|
|
99
|
-
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
|
|
23
|
+
public tileId = -1) {
|
|
100
24
|
}
|
|
101
25
|
}
|
|
102
|
-
const EMPTY_ATTRS = new ExtendedAttrsImage();
|
|
103
|
-
|
|
104
26
|
|
|
105
27
|
/**
|
|
106
28
|
* ImageStorage - extension of CoreTerminal:
|
|
@@ -122,8 +44,11 @@ export class ImageStorage implements IDisposable {
|
|
|
122
44
|
private _needsFullClear = false;
|
|
123
45
|
// hard limit of stored pixels (fallback limit of 10 MB)
|
|
124
46
|
private _pixelLimit: number = 2500000;
|
|
47
|
+
private _workCell: CellData = new CellData();
|
|
125
48
|
|
|
126
49
|
private _viewportMetrics: { cols: number, rows: number };
|
|
50
|
+
public onImageAdded: (() => void) | undefined;
|
|
51
|
+
public onImageDeleted: ((storageId: number) => void) | undefined;
|
|
127
52
|
|
|
128
53
|
constructor(
|
|
129
54
|
private _terminal: ITerminalExt,
|
|
@@ -132,8 +57,10 @@ export class ImageStorage implements IDisposable {
|
|
|
132
57
|
) {
|
|
133
58
|
try {
|
|
134
59
|
this.setLimit(this._opts.storageLimit);
|
|
135
|
-
} catch (e:
|
|
136
|
-
|
|
60
|
+
} catch (e: unknown) {
|
|
61
|
+
if (e instanceof Error) {
|
|
62
|
+
console.error(e.message);
|
|
63
|
+
}
|
|
137
64
|
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
|
138
65
|
}
|
|
139
66
|
this._viewportMetrics = {
|
|
@@ -187,11 +114,13 @@ export class ImageStorage implements IDisposable {
|
|
|
187
114
|
|
|
188
115
|
private _delImg(id: number): void {
|
|
189
116
|
const spec = this._images.get(id);
|
|
117
|
+
if (!spec) return;
|
|
190
118
|
this._images.delete(id);
|
|
191
119
|
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
|
192
|
-
if (
|
|
120
|
+
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
|
193
121
|
spec.orig.close();
|
|
194
122
|
}
|
|
123
|
+
this.onImageDeleted?.(id);
|
|
195
124
|
}
|
|
196
125
|
|
|
197
126
|
/**
|
|
@@ -215,27 +144,29 @@ export class ImageStorage implements IDisposable {
|
|
|
215
144
|
}
|
|
216
145
|
|
|
217
146
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
|
147
|
+
* Delete an image by its internal storage ID.
|
|
148
|
+
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
|
221
149
|
*/
|
|
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
|
-
}
|
|
150
|
+
public deleteImage(id: number): void {
|
|
151
|
+
const spec = this._images.get(id);
|
|
152
|
+
if (spec) {
|
|
153
|
+
spec.marker?.dispose();
|
|
154
|
+
this._delImg(id);
|
|
232
155
|
}
|
|
233
156
|
}
|
|
234
157
|
|
|
235
158
|
/**
|
|
236
159
|
* Method to add an image to the storage.
|
|
160
|
+
* @param img - The image to add (canvas or bitmap).
|
|
161
|
+
* @param opts - Options for addImage:
|
|
162
|
+
* - scrolling: When true, cursor advances with the image.
|
|
163
|
+
* When false, image is placed at ORIGIN and cursor does not move.
|
|
164
|
+
* - layer: Which canvas layer to render on ('top' or 'bottom').
|
|
165
|
+
* - zIndex: Z-index for image layering within the same layer.
|
|
166
|
+
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
|
167
|
+
* @returns The internal image ID assigned to the stored image.
|
|
237
168
|
*/
|
|
238
|
-
public addImage(img: HTMLCanvasElement | ImageBitmap):
|
|
169
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
|
|
239
170
|
// never allow storage to exceed memory limit
|
|
240
171
|
this._evictOldest(img.width * img.height);
|
|
241
172
|
|
|
@@ -257,7 +188,7 @@ export class ImageStorage implements IDisposable {
|
|
|
257
188
|
let offset = originX;
|
|
258
189
|
let tileCount = 0;
|
|
259
190
|
|
|
260
|
-
if (!
|
|
191
|
+
if (!opts.scrolling) {
|
|
261
192
|
buffer.x = 0;
|
|
262
193
|
buffer.y = 0;
|
|
263
194
|
offset = 0;
|
|
@@ -265,13 +196,13 @@ export class ImageStorage implements IDisposable {
|
|
|
265
196
|
|
|
266
197
|
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
|
267
198
|
for (let row = 0; row < rows; ++row) {
|
|
268
|
-
const line = buffer.lines.get(buffer.y + buffer.ybase)
|
|
199
|
+
const line = buffer.lines.get(buffer.y + buffer.ybase)!;
|
|
269
200
|
for (let col = 0; col < cols; ++col) {
|
|
270
201
|
if (offset + col >= termCols) break;
|
|
271
|
-
this._writeToCell(line
|
|
202
|
+
this._writeToCell(line, offset + col, imageId, row * cols + col);
|
|
272
203
|
tileCount++;
|
|
273
204
|
}
|
|
274
|
-
if (
|
|
205
|
+
if (opts.scrolling) {
|
|
275
206
|
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
|
276
207
|
} else {
|
|
277
208
|
if (++buffer.y >= termRows) break;
|
|
@@ -281,8 +212,12 @@ export class ImageStorage implements IDisposable {
|
|
|
281
212
|
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
|
282
213
|
|
|
283
214
|
// cursor positioning modes
|
|
284
|
-
if (
|
|
285
|
-
|
|
215
|
+
if (opts.scrolling) {
|
|
216
|
+
if (opts.cursorPos === 'iip') {
|
|
217
|
+
buffer.x = Math.min(offset + cols, termCols);
|
|
218
|
+
} else {
|
|
219
|
+
buffer.x = offset;
|
|
220
|
+
}
|
|
286
221
|
} else {
|
|
287
222
|
buffer.x = originX;
|
|
288
223
|
buffer.y = originY;
|
|
@@ -324,11 +259,15 @@ export class ImageStorage implements IDisposable {
|
|
|
324
259
|
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
|
325
260
|
marker: endMarker || undefined,
|
|
326
261
|
tileCount,
|
|
327
|
-
bufferType: this._terminal.buffer.active.type
|
|
262
|
+
bufferType: this._terminal.buffer.active.type,
|
|
263
|
+
layer: opts.layer,
|
|
264
|
+
zIndex: opts.zIndex
|
|
328
265
|
};
|
|
329
266
|
|
|
330
267
|
// finally add the image
|
|
331
268
|
this._images.set(imageId, imgSpec);
|
|
269
|
+
this.onImageAdded?.();
|
|
270
|
+
return imageId;
|
|
332
271
|
}
|
|
333
272
|
|
|
334
273
|
|
|
@@ -338,16 +277,30 @@ export class ImageStorage implements IDisposable {
|
|
|
338
277
|
*/
|
|
339
278
|
// TODO: Should we move this to the ImageRenderer?
|
|
340
279
|
public render(range: { start: number, end: number }): void {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
280
|
+
// Determine which layers have images
|
|
281
|
+
let hasTopImages = false;
|
|
282
|
+
let hasBottomImages = false;
|
|
283
|
+
for (const spec of this._images.values()) {
|
|
284
|
+
if (spec.layer === 'bottom') {
|
|
285
|
+
hasBottomImages = true;
|
|
286
|
+
} else {
|
|
287
|
+
hasTopImages = true;
|
|
347
288
|
}
|
|
289
|
+
if (hasTopImages && hasBottomImages) break;
|
|
348
290
|
}
|
|
291
|
+
|
|
292
|
+
// Lazily insert layers that are needed
|
|
293
|
+
if (hasTopImages && !this._renderer.hasLayer('top')) {
|
|
294
|
+
this._renderer.insertLayerToDom('top');
|
|
295
|
+
if (!this._renderer.hasLayer('top')) return;
|
|
296
|
+
}
|
|
297
|
+
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
|
|
298
|
+
this._renderer.insertLayerToDom('bottom');
|
|
299
|
+
}
|
|
300
|
+
|
|
349
301
|
// rescale if needed
|
|
350
302
|
this._renderer.rescaleCanvas();
|
|
303
|
+
|
|
351
304
|
// exit early if we dont have any images to test for
|
|
352
305
|
if (!this._images.size) {
|
|
353
306
|
if (!this._fullyCleared) {
|
|
@@ -355,12 +308,25 @@ export class ImageStorage implements IDisposable {
|
|
|
355
308
|
this._fullyCleared = true;
|
|
356
309
|
this._needsFullClear = false;
|
|
357
310
|
}
|
|
358
|
-
if (this._renderer.
|
|
359
|
-
this._renderer.removeLayerFromDom();
|
|
311
|
+
if (this._renderer.hasLayer('top')) {
|
|
312
|
+
this._renderer.removeLayerFromDom('top');
|
|
313
|
+
}
|
|
314
|
+
if (this._renderer.hasLayer('bottom')) {
|
|
315
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
360
316
|
}
|
|
361
317
|
return;
|
|
362
318
|
}
|
|
363
319
|
|
|
320
|
+
// Remove layers no longer needed
|
|
321
|
+
if (!hasTopImages && this._renderer.hasLayer('top')) {
|
|
322
|
+
this._renderer.clearAll('top');
|
|
323
|
+
this._renderer.removeLayerFromDom('top');
|
|
324
|
+
}
|
|
325
|
+
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
|
|
326
|
+
this._renderer.clearAll('bottom');
|
|
327
|
+
this._renderer.removeLayerFromDom('bottom');
|
|
328
|
+
}
|
|
329
|
+
|
|
364
330
|
// buffer switches force a full clear
|
|
365
331
|
if (this._needsFullClear) {
|
|
366
332
|
this._renderer.clearAll();
|
|
@@ -375,13 +341,17 @@ export class ImageStorage implements IDisposable {
|
|
|
375
341
|
// clear drawing area
|
|
376
342
|
this._renderer.clearLines(start, end);
|
|
377
343
|
|
|
378
|
-
//
|
|
344
|
+
// Collect draw calls so we can sort by z-index (lower z drawn first).
|
|
345
|
+
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
|
|
346
|
+
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
|
347
|
+
|
|
348
|
+
// walk all cells in viewport and collect tiles found
|
|
379
349
|
for (let row = start; row <= end; ++row) {
|
|
380
|
-
const line = buffer.lines.get(row + buffer.ydisp)
|
|
350
|
+
const line = buffer.lines.get(row + buffer.ydisp);
|
|
381
351
|
if (!line) return;
|
|
382
352
|
for (let col = 0; col < cols; ++col) {
|
|
383
|
-
|
|
384
|
-
|
|
353
|
+
const e = line.getExtended(col)?.payload;
|
|
354
|
+
if (e instanceof ImageTileInfo) {
|
|
385
355
|
const imageId = e.imageId;
|
|
386
356
|
if (imageId === undefined || imageId === -1) {
|
|
387
357
|
continue;
|
|
@@ -397,28 +367,39 @@ export class ImageStorage implements IDisposable {
|
|
|
397
367
|
* - cell has same image id
|
|
398
368
|
* - cell has consecutive tile id
|
|
399
369
|
*/
|
|
400
|
-
while (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
&& (e.tileId === startTile + count)
|
|
406
|
-
) {
|
|
370
|
+
while (++col < cols) {
|
|
371
|
+
const nextE = line.getExtended(col)?.payload;
|
|
372
|
+
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
407
375
|
count++;
|
|
408
376
|
}
|
|
409
377
|
col--;
|
|
410
378
|
if (imgSpec) {
|
|
411
379
|
if (imgSpec.actual) {
|
|
412
|
-
|
|
380
|
+
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
|
413
381
|
}
|
|
414
382
|
} else if (this._opts.showPlaceholder) {
|
|
415
|
-
|
|
383
|
+
placeholderCalls.push({ col: startCol, row, count });
|
|
416
384
|
}
|
|
417
385
|
this._fullyCleared = false;
|
|
418
386
|
}
|
|
419
387
|
}
|
|
420
388
|
}
|
|
421
389
|
}
|
|
390
|
+
|
|
391
|
+
// Sort by z-index so lower z draws first (higher z renders on top)
|
|
392
|
+
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
|
|
393
|
+
|
|
394
|
+
// Draw placeholders first (lowest priority)
|
|
395
|
+
for (const call of placeholderCalls) {
|
|
396
|
+
this._renderer.drawPlaceholder(call.col, call.row, call.count);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Draw images in z-index order
|
|
400
|
+
for (const call of drawCalls) {
|
|
401
|
+
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
|
|
402
|
+
}
|
|
422
403
|
}
|
|
423
404
|
|
|
424
405
|
public viewportResize(metrics: { cols: number, rows: number }): void {
|
|
@@ -440,9 +421,9 @@ export class ImageStorage implements IDisposable {
|
|
|
440
421
|
const rows = buffer.lines.length;
|
|
441
422
|
const oldCol = this._viewportMetrics.cols - 1;
|
|
442
423
|
for (let row = 0; row < rows; ++row) {
|
|
443
|
-
const line = buffer.lines.get(row)
|
|
444
|
-
|
|
445
|
-
|
|
424
|
+
const line = buffer.lines.get(row)!;
|
|
425
|
+
const e = line.getExtended(oldCol)?.payload;
|
|
426
|
+
if (e instanceof ImageTileInfo) {
|
|
446
427
|
const imageId = e.imageId;
|
|
447
428
|
if (imageId === undefined || imageId === -1) {
|
|
448
429
|
continue;
|
|
@@ -459,7 +440,7 @@ export class ImageStorage implements IDisposable {
|
|
|
459
440
|
// expand only if right side is empty (nothing got wrapped from below)
|
|
460
441
|
let hasData = false;
|
|
461
442
|
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
|
|
462
|
-
if (line.
|
|
443
|
+
if (line.hasContent(rightCol)) {
|
|
463
444
|
hasData = true;
|
|
464
445
|
break;
|
|
465
446
|
}
|
|
@@ -471,7 +452,7 @@ export class ImageStorage implements IDisposable {
|
|
|
471
452
|
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
|
472
453
|
let lastTile = e.tileId;
|
|
473
454
|
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
|
474
|
-
this._writeToCell(line
|
|
455
|
+
this._writeToCell(line, expandCol, imageId, ++lastTile);
|
|
475
456
|
imgSpec.tileCount++;
|
|
476
457
|
}
|
|
477
458
|
}
|
|
@@ -485,10 +466,10 @@ export class ImageStorage implements IDisposable {
|
|
|
485
466
|
*/
|
|
486
467
|
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
487
468
|
const buffer = this._terminal._core.buffer;
|
|
488
|
-
const line = buffer.lines.get(y)
|
|
489
|
-
if (line
|
|
490
|
-
const e
|
|
491
|
-
if (e.imageId && e.imageId !== -1) {
|
|
469
|
+
const line = buffer.lines.get(y);
|
|
470
|
+
if (line) {
|
|
471
|
+
const e = line.getExtended(x)?.payload;
|
|
472
|
+
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
|
|
492
473
|
const orig = this._images.get(e.imageId)?.orig;
|
|
493
474
|
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
|
494
475
|
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
|
@@ -505,10 +486,10 @@ export class ImageStorage implements IDisposable {
|
|
|
505
486
|
*/
|
|
506
487
|
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
507
488
|
const buffer = this._terminal._core.buffer;
|
|
508
|
-
const line = buffer.lines.get(y)
|
|
509
|
-
if (line
|
|
510
|
-
const e
|
|
511
|
-
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
489
|
+
const line = buffer.lines.get(y);
|
|
490
|
+
if (line) {
|
|
491
|
+
const e = line.getExtended(x)?.payload;
|
|
492
|
+
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
512
493
|
const spec = this._images.get(e.imageId);
|
|
513
494
|
if (spec) {
|
|
514
495
|
return this._renderer.extractTile(spec, e.tileId);
|
|
@@ -536,31 +517,31 @@ export class ImageStorage implements IDisposable {
|
|
|
536
517
|
return used - current;
|
|
537
518
|
}
|
|
538
519
|
|
|
539
|
-
private _writeToCell(line:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
old.imageId = imageId;
|
|
553
|
-
old.tileId = tileId;
|
|
554
|
-
return;
|
|
520
|
+
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void {
|
|
521
|
+
const workCell = this._workCell;
|
|
522
|
+
line.loadCell(x, workCell);
|
|
523
|
+
if (this._workCell.hasExtendedAttrs()) {
|
|
524
|
+
const old = workCell.extended.payload;
|
|
525
|
+
if (old instanceof ImageTileInfo) {
|
|
526
|
+
// found an old ExtendedAttrsImage, since we know that
|
|
527
|
+
// they are always isolated instances (single cell usage),
|
|
528
|
+
// we can re-use it and just update their id entries
|
|
529
|
+
const oldSpec = this._images.get(old.imageId);
|
|
530
|
+
if (oldSpec) {
|
|
531
|
+
// early eviction for in-viewport overwrites
|
|
532
|
+
oldSpec.tileCount--;
|
|
555
533
|
}
|
|
556
|
-
|
|
557
|
-
|
|
534
|
+
old.imageId = imageId;
|
|
535
|
+
old.tileId = tileId;
|
|
558
536
|
return;
|
|
559
537
|
}
|
|
560
538
|
}
|
|
561
|
-
// fall-through: always create new
|
|
562
|
-
|
|
563
|
-
|
|
539
|
+
// fall-through: always create new ExtendedAttrs entry
|
|
540
|
+
const extattr = workCell.extended.clone();
|
|
541
|
+
extattr.payload = new ImageTileInfo(imageId, tileId);
|
|
542
|
+
workCell.extended = extattr;
|
|
543
|
+
workCell.updateExtended();
|
|
544
|
+
line.setCell(x, workCell);
|
|
564
545
|
}
|
|
565
546
|
|
|
566
547
|
private _evictOnAlternate(): void {
|
|
@@ -573,18 +554,16 @@ export class ImageStorage implements IDisposable {
|
|
|
573
554
|
// re-count tiles on whole buffer
|
|
574
555
|
const buffer = this._terminal._core.buffer;
|
|
575
556
|
for (let y = 0; y < this._terminal.rows; ++y) {
|
|
576
|
-
const line = buffer.lines.get(y)
|
|
557
|
+
const line = buffer.lines.get(y);
|
|
577
558
|
if (!line) {
|
|
578
559
|
continue;
|
|
579
560
|
}
|
|
580
561
|
for (let x = 0; x < this._terminal.cols; ++x) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
spec.tileCount++;
|
|
587
|
-
}
|
|
562
|
+
const payload = line.getExtended(x)?.payload;
|
|
563
|
+
if (payload instanceof ImageTileInfo) {
|
|
564
|
+
const spec = this._images.get(payload.imageId);
|
|
565
|
+
if (spec) {
|
|
566
|
+
spec.tileCount++;
|
|
588
567
|
}
|
|
589
568
|
}
|
|
590
569
|
}
|
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
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
7
|
+
import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
|
|
8
|
+
import { ImageRenderer } from './ImageRenderer';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sixel-specific image storage controller.
|
|
12
|
+
*
|
|
13
|
+
* Wraps the shared ImageStorage with sixel protocol semantics:
|
|
14
|
+
* - Cursor behavior governed by DECSET 80 (sixelScrolling option)
|
|
15
|
+
* - advanceCursor for empty sixels carrying only height
|
|
16
|
+
*/
|
|
17
|
+
export class SixelImageStorage {
|
|
18
|
+
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'vt340' };
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly _storage: ImageStorage,
|
|
21
|
+
private readonly _opts: IImageAddonOptions,
|
|
22
|
+
private readonly _renderer: ImageRenderer,
|
|
23
|
+
private readonly _terminal: ITerminalExt
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add a sixel image to storage.
|
|
28
|
+
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
|
29
|
+
*/
|
|
30
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
|
31
|
+
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
|
32
|
+
this._storage.addImage(img, this._addImageOpts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Only advance text cursor.
|
|
37
|
+
* This is an edge case from empty sixels carrying only a height but no pixels.
|
|
38
|
+
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
|
39
|
+
*/
|
|
40
|
+
public advanceCursor(height: number): void {
|
|
41
|
+
if (this._opts.sixelScrolling) {
|
|
42
|
+
let cellSize = this._renderer.cellSize;
|
|
43
|
+
if (cellSize.width === -1 || cellSize.height === -1) {
|
|
44
|
+
cellSize = CELL_SIZE_DEFAULT;
|
|
45
|
+
}
|
|
46
|
+
const rows = Math.ceil(height / cellSize.height);
|
|
47
|
+
for (let i = 1; i < rows; ++i) {
|
|
48
|
+
this._terminal._core._inputHandler.lineFeed();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|