@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.
@@ -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 'vs/base/common/lifecycle';
8
+ import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
9
+ import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
10
10
 
11
- const PLACEHOLDER_LENGTH = 4096;
12
- const PLACEHOLDER_HEIGHT = 24;
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
- public canvas: HTMLCanvasElement | undefined;
22
- private _ctx: CanvasRenderingContext2D | null | undefined;
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 || document).createElement('canvas');
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.canvas = undefined;
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._renderService?.dimensions;
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._ctx?.clearRect(
145
- 0,
146
- start * (this.dimensions?.css.cell.height || 0),
147
- this.dimensions?.css.canvas.width || 0,
148
- (++end - start) * (this.dimensions?.css.cell.height || 0)
149
- );
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
- this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
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
- if (!this._ctx) {
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
- this._ctx.drawImage(
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
- if (this._ctx) {
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
- this._ctx.drawImage(
245
- this._placeholderBitmap || this._placeholder!,
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
- if (!this.canvas) {
264
- return;
265
- }
266
- if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
267
- this.canvas.width = this.dimensions!.css.canvas.width || 0;
268
- this.canvas.height = this.dimensions!.css.canvas.height || 0;
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.removeLayerFromDom();
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 && this._terminal._core.screenElement) {
315
- if (!this.canvas) {
316
- this.canvas = ImageRenderer.createCanvas(
317
- this.document, this.dimensions?.css.canvas.width || 0,
318
- this.dimensions?.css.canvas.height || 0
319
- );
320
- this.canvas.classList.add('xterm-image-layer');
321
- this._terminal._core.screenElement.appendChild(this.canvas);
322
- this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
323
- this.clearAll();
324
- }
325
- } else {
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
- if (this.canvas) {
332
- this._ctx = undefined;
333
- this.canvas.remove();
334
- this.canvas = undefined;
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
- private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
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) {
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { IDisposable } from '@xterm/xterm';
7
7
  import { ImageRenderer } from './ImageRenderer';
8
- import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
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: any) {
136
- console.error(e.message);
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 (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
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
- * Only advance text cursor.
219
- * This is an edge case from empty sixels carrying only a height but no pixels.
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 advanceCursor(height: number): void {
223
- if (this._opts.sixelScrolling) {
224
- let cellSize = this._renderer.cellSize;
225
- if (cellSize.width === -1 || cellSize.height === -1) {
226
- cellSize = CELL_SIZE_DEFAULT;
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): void {
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 (!this._opts.sixelScrolling) {
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 (this._opts.sixelScrolling) {
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 (this._opts.sixelScrolling) {
285
- buffer.x = offset;
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
- // setup image canvas in case we have none yet, but have images in store
342
- if (!this._renderer.canvas && this._images.size) {
343
- this._renderer.insertLayerToDom();
344
- // safety measure - in case we cannot spawn a canvas at all, just exit
345
- if (!this._renderer.canvas) {
346
- return;
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.canvas) {
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
- // walk all cells in viewport and draw tiles found
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
- let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
385
- const imageId = e.imageId;
386
- if (imageId === undefined || imageId === -1) {
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
- const imgSpec = this._images.get(imageId);
390
- if (e.tileId !== -1) {
391
- const startTile = e.tileId;
392
- const startCol = col;
393
- let count = 1;
394
- /**
395
- * merge tiles to the right into a single draw call, if:
396
- * - not at end of line
397
- * - cell has same image id
398
- * - cell has consecutive tile id
399
- */
400
- while (
401
- ++col < cols
402
- && (line.getBg(col) & BgFlags.HAS_EXTENDED)
403
- && (e = line._extendedAttrs[col] || EMPTY_ATTRS)
404
- && (e.imageId === imageId)
405
- && (e.tileId === startTile + count)
406
- ) {
407
- count++;
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
- col--;
410
- if (imgSpec) {
411
- if (imgSpec.actual) {
412
- this._renderer.draw(imgSpec, startTile, startCol, row, count);
413
- }
414
- } else if (this._opts.showPlaceholder) {
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
- this._fullyCleared = false;
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] || EMPTY_ATTRS;
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] || EMPTY_ATTRS;
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] || EMPTY_ATTRS;
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) {
@@ -3,7 +3,7 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { ImageStorage } from './ImageStorage';
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: ImageStorage,
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 || ! height) {
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, width, height), 0, 0);
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
  }