@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,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDisposable } from '@xterm/xterm';
|
|
7
|
+
import { ImageRenderer } from './ImageRenderer';
|
|
8
|
+
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// fallback default cell size
|
|
12
|
+
export const CELL_SIZE_DEFAULT: ICellSize = {
|
|
13
|
+
width: 7,
|
|
14
|
+
height: 14
|
|
15
|
+
};
|
|
16
|
+
|
|
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
|
+
private _urlId: number = 0;
|
|
57
|
+
public get urlId(): number {
|
|
58
|
+
return this._urlId;
|
|
59
|
+
}
|
|
60
|
+
public set urlId(value: number) {
|
|
61
|
+
this._urlId = value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
ext: number = 0,
|
|
66
|
+
urlId: number = 0,
|
|
67
|
+
public imageId = -1,
|
|
68
|
+
public tileId = -1
|
|
69
|
+
) {
|
|
70
|
+
this._ext = ext;
|
|
71
|
+
this._urlId = urlId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public clone(): IExtendedAttrsImage {
|
|
75
|
+
/**
|
|
76
|
+
* Technically we dont need a clone variant of ExtendedAttrsImage,
|
|
77
|
+
* as we never clone a cell holding image data.
|
|
78
|
+
* Note: Clone is only meant to be used by the InputHandler for
|
|
79
|
+
* sticky attributes, which is never the case for image data.
|
|
80
|
+
* We still provide a proper clone method to reflect the full ext attr
|
|
81
|
+
* state in case there are future use cases for clone.
|
|
82
|
+
*/
|
|
83
|
+
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public isEmpty(): boolean {
|
|
87
|
+
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const EMPTY_ATTRS = new ExtendedAttrsImage();
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ImageStorage - extension of CoreTerminal:
|
|
95
|
+
* - hold image data
|
|
96
|
+
* - write/read image data to/from buffer
|
|
97
|
+
*
|
|
98
|
+
* TODO: image composition for overwrites
|
|
99
|
+
*/
|
|
100
|
+
export class ImageStorage implements IDisposable {
|
|
101
|
+
// storage
|
|
102
|
+
private _images: Map<number, IImageSpec> = new Map();
|
|
103
|
+
// last used id
|
|
104
|
+
private _lastId = 0;
|
|
105
|
+
// last evicted id
|
|
106
|
+
private _lowestId = 0;
|
|
107
|
+
// whether a full clear happened before
|
|
108
|
+
private _fullyCleared = false;
|
|
109
|
+
// whether render should do a full clear
|
|
110
|
+
private _needsFullClear = false;
|
|
111
|
+
// hard limit of stored pixels (fallback limit of 10 MB)
|
|
112
|
+
private _pixelLimit: number = 2500000;
|
|
113
|
+
|
|
114
|
+
private _viewportMetrics: { cols: number, rows: number };
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
private _terminal: ITerminalExt,
|
|
118
|
+
private _renderer: ImageRenderer,
|
|
119
|
+
private _opts: IImageAddonOptions
|
|
120
|
+
) {
|
|
121
|
+
try {
|
|
122
|
+
this.setLimit(this._opts.storageLimit);
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
console.error(e.message);
|
|
125
|
+
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
|
126
|
+
}
|
|
127
|
+
this._viewportMetrics = {
|
|
128
|
+
cols: this._terminal.cols,
|
|
129
|
+
rows: this._terminal.rows
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public dispose(): void {
|
|
134
|
+
this.reset();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public reset(): void {
|
|
138
|
+
for (const spec of this._images.values()) {
|
|
139
|
+
spec.marker?.dispose();
|
|
140
|
+
}
|
|
141
|
+
// NOTE: marker.dispose above already calls ImageBitmap.close
|
|
142
|
+
// therefore we can just wipe the map here
|
|
143
|
+
this._images.clear();
|
|
144
|
+
this._renderer.clearAll();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public getLimit(): number {
|
|
148
|
+
return this._pixelLimit * 4 / 1000000;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public setLimit(value: number): void {
|
|
152
|
+
if (value < 0.5 || value > 1000) {
|
|
153
|
+
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
|
|
154
|
+
}
|
|
155
|
+
this._pixelLimit = (value / 4 * 1000000) >>> 0;
|
|
156
|
+
this._evictOldest(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public getUsage(): number {
|
|
160
|
+
return this._getStoredPixels() * 4 / 1000000;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private _getStoredPixels(): number {
|
|
164
|
+
let storedPixels = 0;
|
|
165
|
+
for (const spec of this._images.values()) {
|
|
166
|
+
if (spec.orig) {
|
|
167
|
+
storedPixels += spec.orig.width * spec.orig.height;
|
|
168
|
+
if (spec.actual && spec.actual !== spec.orig) {
|
|
169
|
+
storedPixels += spec.actual.width * spec.actual.height;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return storedPixels;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private _delImg(id: number): void {
|
|
177
|
+
const spec = this._images.get(id);
|
|
178
|
+
this._images.delete(id);
|
|
179
|
+
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
|
180
|
+
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
|
181
|
+
spec.orig.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Wipe canvas and images on alternate buffer.
|
|
187
|
+
*/
|
|
188
|
+
public wipeAlternate(): void {
|
|
189
|
+
// remove all alternate tagged images
|
|
190
|
+
const zero = [];
|
|
191
|
+
for (const [id, spec] of this._images.entries()) {
|
|
192
|
+
if (spec.bufferType === 'alternate') {
|
|
193
|
+
spec.marker?.dispose();
|
|
194
|
+
zero.push(id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const id of zero) {
|
|
198
|
+
this._delImg(id);
|
|
199
|
+
}
|
|
200
|
+
// mark canvas to be wiped on next render
|
|
201
|
+
this._needsFullClear = true;
|
|
202
|
+
this._fullyCleared = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Only advance text cursor.
|
|
207
|
+
* This is an edge case from empty sixels carrying only a height but no pixels.
|
|
208
|
+
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
|
209
|
+
*/
|
|
210
|
+
public advanceCursor(height: number): void {
|
|
211
|
+
if (this._opts.sixelScrolling) {
|
|
212
|
+
let cellSize = this._renderer.cellSize;
|
|
213
|
+
if (cellSize.width === -1 || cellSize.height === -1) {
|
|
214
|
+
cellSize = CELL_SIZE_DEFAULT;
|
|
215
|
+
}
|
|
216
|
+
const rows = Math.ceil(height / cellSize.height);
|
|
217
|
+
for (let i = 1; i < rows; ++i) {
|
|
218
|
+
this._terminal._core._inputHandler.lineFeed();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Method to add an image to the storage.
|
|
225
|
+
*/
|
|
226
|
+
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
|
227
|
+
// never allow storage to exceed memory limit
|
|
228
|
+
this._evictOldest(img.width * img.height);
|
|
229
|
+
|
|
230
|
+
// calc rows x cols needed to display the image
|
|
231
|
+
let cellSize = this._renderer.cellSize;
|
|
232
|
+
if (cellSize.width === -1 || cellSize.height === -1) {
|
|
233
|
+
cellSize = CELL_SIZE_DEFAULT;
|
|
234
|
+
}
|
|
235
|
+
const cols = Math.ceil(img.width / cellSize.width);
|
|
236
|
+
const rows = Math.ceil(img.height / cellSize.height);
|
|
237
|
+
|
|
238
|
+
const imageId = ++this._lastId;
|
|
239
|
+
|
|
240
|
+
const buffer = this._terminal._core.buffer;
|
|
241
|
+
const termCols = this._terminal.cols;
|
|
242
|
+
const termRows = this._terminal.rows;
|
|
243
|
+
const originX = buffer.x;
|
|
244
|
+
const originY = buffer.y;
|
|
245
|
+
let offset = originX;
|
|
246
|
+
let tileCount = 0;
|
|
247
|
+
|
|
248
|
+
if (!this._opts.sixelScrolling) {
|
|
249
|
+
buffer.x = 0;
|
|
250
|
+
buffer.y = 0;
|
|
251
|
+
offset = 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
|
255
|
+
for (let row = 0; row < rows; ++row) {
|
|
256
|
+
const line = buffer.lines.get(buffer.y + buffer.ybase);
|
|
257
|
+
for (let col = 0; col < cols; ++col) {
|
|
258
|
+
if (offset + col >= termCols) break;
|
|
259
|
+
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
|
260
|
+
tileCount++;
|
|
261
|
+
}
|
|
262
|
+
if (this._opts.sixelScrolling) {
|
|
263
|
+
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
|
264
|
+
} else {
|
|
265
|
+
if (++buffer.y >= termRows) break;
|
|
266
|
+
}
|
|
267
|
+
buffer.x = offset;
|
|
268
|
+
}
|
|
269
|
+
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
|
270
|
+
|
|
271
|
+
// cursor positioning modes
|
|
272
|
+
if (this._opts.sixelScrolling) {
|
|
273
|
+
buffer.x = offset;
|
|
274
|
+
} else {
|
|
275
|
+
buffer.x = originX;
|
|
276
|
+
buffer.y = originY;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// deleted images with zero tile count
|
|
280
|
+
const zero = [];
|
|
281
|
+
for (const [id, spec] of this._images.entries()) {
|
|
282
|
+
if (spec.tileCount < 1) {
|
|
283
|
+
spec.marker?.dispose();
|
|
284
|
+
zero.push(id);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const id of zero) {
|
|
288
|
+
this._delImg(id);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// eviction marker:
|
|
292
|
+
// delete the image when the marker gets disposed
|
|
293
|
+
const endMarker = this._terminal.registerMarker(0);
|
|
294
|
+
endMarker?.onDispose(() => {
|
|
295
|
+
const spec = this._images.get(imageId);
|
|
296
|
+
if (spec) {
|
|
297
|
+
this._delImg(imageId);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// since markers do not work on alternate for some reason,
|
|
302
|
+
// we evict images here manually
|
|
303
|
+
if (this._terminal.buffer.active.type === 'alternate') {
|
|
304
|
+
this._evictOnAlternate();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// create storage entry
|
|
308
|
+
const imgSpec: IImageSpec = {
|
|
309
|
+
orig: img,
|
|
310
|
+
origCellSize: cellSize,
|
|
311
|
+
actual: img,
|
|
312
|
+
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
|
313
|
+
marker: endMarker || undefined,
|
|
314
|
+
tileCount,
|
|
315
|
+
bufferType: this._terminal.buffer.active.type
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// finally add the image
|
|
319
|
+
this._images.set(imageId, imgSpec);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Render method. Collects buffer information and triggers
|
|
325
|
+
* canvas updates.
|
|
326
|
+
*/
|
|
327
|
+
// TODO: Should we move this to the ImageRenderer?
|
|
328
|
+
public render(range: { start: number, end: number }): void {
|
|
329
|
+
// setup image canvas in case we have none yet, but have images in store
|
|
330
|
+
if (!this._renderer.canvas && this._images.size) {
|
|
331
|
+
this._renderer.insertLayerToDom();
|
|
332
|
+
// safety measure - in case we cannot spawn a canvas at all, just exit
|
|
333
|
+
if (!this._renderer.canvas) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// rescale if needed
|
|
338
|
+
this._renderer.rescaleCanvas();
|
|
339
|
+
// exit early if we dont have any images to test for
|
|
340
|
+
if (!this._images.size) {
|
|
341
|
+
if (!this._fullyCleared) {
|
|
342
|
+
this._renderer.clearAll();
|
|
343
|
+
this._fullyCleared = true;
|
|
344
|
+
this._needsFullClear = false;
|
|
345
|
+
}
|
|
346
|
+
if (this._renderer.canvas) {
|
|
347
|
+
this._renderer.removeLayerFromDom();
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// buffer switches force a full clear
|
|
353
|
+
if (this._needsFullClear) {
|
|
354
|
+
this._renderer.clearAll();
|
|
355
|
+
this._fullyCleared = true;
|
|
356
|
+
this._needsFullClear = false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { start, end } = range;
|
|
360
|
+
const buffer = this._terminal._core.buffer;
|
|
361
|
+
const cols = this._terminal._core.cols;
|
|
362
|
+
|
|
363
|
+
// clear drawing area
|
|
364
|
+
this._renderer.clearLines(start, end);
|
|
365
|
+
|
|
366
|
+
// walk all cells in viewport and draw tiles found
|
|
367
|
+
for (let row = start; row <= end; ++row) {
|
|
368
|
+
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
|
369
|
+
if (!line) return;
|
|
370
|
+
for (let col = 0; col < cols; ++col) {
|
|
371
|
+
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
|
372
|
+
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
|
373
|
+
const imageId = e.imageId;
|
|
374
|
+
if (imageId === undefined || imageId === -1) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const imgSpec = this._images.get(imageId);
|
|
378
|
+
if (e.tileId !== -1) {
|
|
379
|
+
const startTile = e.tileId;
|
|
380
|
+
const startCol = col;
|
|
381
|
+
let count = 1;
|
|
382
|
+
/**
|
|
383
|
+
* merge tiles to the right into a single draw call, if:
|
|
384
|
+
* - not at end of line
|
|
385
|
+
* - cell has same image id
|
|
386
|
+
* - cell has consecutive tile id
|
|
387
|
+
*/
|
|
388
|
+
while (
|
|
389
|
+
++col < cols
|
|
390
|
+
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
|
391
|
+
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
|
392
|
+
&& (e.imageId === imageId)
|
|
393
|
+
&& (e.tileId === startTile + count)
|
|
394
|
+
) {
|
|
395
|
+
count++;
|
|
396
|
+
}
|
|
397
|
+
col--;
|
|
398
|
+
if (imgSpec) {
|
|
399
|
+
if (imgSpec.actual) {
|
|
400
|
+
this._renderer.draw(imgSpec, startTile, startCol, row, count);
|
|
401
|
+
}
|
|
402
|
+
} else if (this._opts.showPlaceholder) {
|
|
403
|
+
this._renderer.drawPlaceholder(startCol, row, count);
|
|
404
|
+
}
|
|
405
|
+
this._fullyCleared = false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public viewportResize(metrics: { cols: number, rows: number }): void {
|
|
413
|
+
// exit early if we have nothing in storage
|
|
414
|
+
if (!this._images.size) {
|
|
415
|
+
this._viewportMetrics = metrics;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// handle only viewport width enlargements, exit all other cases
|
|
420
|
+
// TODO: needs patch for tile counter
|
|
421
|
+
if (this._viewportMetrics.cols >= metrics.cols) {
|
|
422
|
+
this._viewportMetrics = metrics;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// walk scrollbuffer at old col width to find all possible expansion matches
|
|
427
|
+
const buffer = this._terminal._core.buffer;
|
|
428
|
+
const rows = buffer.lines.length;
|
|
429
|
+
const oldCol = this._viewportMetrics.cols - 1;
|
|
430
|
+
for (let row = 0; row < rows; ++row) {
|
|
431
|
+
const line = buffer.lines.get(row) as IBufferLineExt;
|
|
432
|
+
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
|
433
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
|
|
434
|
+
const imageId = e.imageId;
|
|
435
|
+
if (imageId === undefined || imageId === -1) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const imgSpec = this._images.get(imageId);
|
|
439
|
+
if (!imgSpec) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// found an image tile at oldCol, check if it qualifies for right exapansion
|
|
443
|
+
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
|
|
444
|
+
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// expand only if right side is empty (nothing got wrapped from below)
|
|
448
|
+
let hasData = false;
|
|
449
|
+
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
|
|
450
|
+
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
|
|
451
|
+
hasData = true;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (hasData) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// do right expansion on terminal buffer
|
|
459
|
+
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
|
460
|
+
let lastTile = e.tileId;
|
|
461
|
+
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
|
462
|
+
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
|
|
463
|
+
imgSpec.tileCount++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// store new viewport metrics
|
|
468
|
+
this._viewportMetrics = metrics;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Retrieve original canvas at buffer position.
|
|
473
|
+
*/
|
|
474
|
+
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
475
|
+
const buffer = this._terminal._core.buffer;
|
|
476
|
+
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
477
|
+
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
|
478
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
|
479
|
+
if (e.imageId && e.imageId !== -1) {
|
|
480
|
+
const orig = this._images.get(e.imageId)?.orig;
|
|
481
|
+
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
|
482
|
+
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
|
483
|
+
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
|
|
484
|
+
return canvas;
|
|
485
|
+
}
|
|
486
|
+
return orig as HTMLCanvasElement;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Extract active single tile at buffer position.
|
|
493
|
+
*/
|
|
494
|
+
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
495
|
+
const buffer = this._terminal._core.buffer;
|
|
496
|
+
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
497
|
+
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
|
498
|
+
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
|
499
|
+
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
500
|
+
const spec = this._images.get(e.imageId);
|
|
501
|
+
if (spec) {
|
|
502
|
+
return this._renderer.extractTile(spec, e.tileId);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
|
509
|
+
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
|
510
|
+
private _evictOldest(room: number): number {
|
|
511
|
+
const used = this._getStoredPixels();
|
|
512
|
+
let current = used;
|
|
513
|
+
while (this._pixelLimit < current + room && this._images.size) {
|
|
514
|
+
const spec = this._images.get(++this._lowestId);
|
|
515
|
+
if (spec && spec.orig) {
|
|
516
|
+
current -= spec.orig.width * spec.orig.height;
|
|
517
|
+
if (spec.actual && spec.orig !== spec.actual) {
|
|
518
|
+
current -= spec.actual.width * spec.actual.height;
|
|
519
|
+
}
|
|
520
|
+
spec.marker?.dispose();
|
|
521
|
+
this._delImg(this._lowestId);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return used - current;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
|
|
528
|
+
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
|
529
|
+
const old = line._extendedAttrs[x];
|
|
530
|
+
if (old) {
|
|
531
|
+
if (old.imageId !== undefined) {
|
|
532
|
+
// found an old ExtendedAttrsImage, since we know that
|
|
533
|
+
// they are always isolated instances (single cell usage),
|
|
534
|
+
// we can re-use it and just update their id entries
|
|
535
|
+
const oldSpec = this._images.get(old.imageId);
|
|
536
|
+
if (oldSpec) {
|
|
537
|
+
// early eviction for in-viewport overwrites
|
|
538
|
+
oldSpec.tileCount--;
|
|
539
|
+
}
|
|
540
|
+
old.imageId = imageId;
|
|
541
|
+
old.tileId = tileId;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// found a plain ExtendedAttrs instance, clone it to new entry
|
|
545
|
+
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// fall-through: always create new ExtendedAttrsImage entry
|
|
550
|
+
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
|
|
551
|
+
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private _evictOnAlternate(): void {
|
|
555
|
+
// nullify tile count of all images on alternate buffer
|
|
556
|
+
for (const spec of this._images.values()) {
|
|
557
|
+
if (spec.bufferType === 'alternate') {
|
|
558
|
+
spec.tileCount = 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// re-count tiles on whole buffer
|
|
562
|
+
const buffer = this._terminal._core.buffer;
|
|
563
|
+
for (let y = 0; y < this._terminal.rows; ++y) {
|
|
564
|
+
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
565
|
+
if (!line) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
for (let x = 0; x < this._terminal.cols; ++x) {
|
|
569
|
+
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
|
570
|
+
const imgId = line._extendedAttrs[x]?.imageId;
|
|
571
|
+
if (imgId) {
|
|
572
|
+
const spec = this._images.get(imgId);
|
|
573
|
+
if (spec) {
|
|
574
|
+
spec.tileCount++;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// deleted images with zero tile count
|
|
581
|
+
const zero = [];
|
|
582
|
+
for (const [id, spec] of this._images.entries()) {
|
|
583
|
+
if (spec.bufferType === 'alternate' && !spec.tileCount) {
|
|
584
|
+
spec.marker?.dispose();
|
|
585
|
+
zero.push(id);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const id of zero) {
|
|
589
|
+
this._delImg(id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|