@xterm/addon-image 0.10.0-beta.166 → 0.10.0-beta.167
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xterm/addon-image",
|
|
3
|
-
"version": "0.10.0-beta.
|
|
3
|
+
"version": "0.10.0-beta.167",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "The xterm.js authors",
|
|
6
6
|
"url": "https://xtermjs.org/"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"sixel": "^0.16.0",
|
|
28
28
|
"xterm-wasm-parts": "^0.3.0"
|
|
29
29
|
},
|
|
30
|
-
"commit": "
|
|
30
|
+
"commit": "dedf3b15956b5185eae84d49f3fbd7593a2fc0fc",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
32
|
+
"@xterm/xterm": "^6.1.0-beta.167"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -323,9 +323,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
323
323
|
case KittyAction.QUERY:
|
|
324
324
|
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
|
|
325
325
|
return true;
|
|
326
|
+
case KittyAction.PLACEMENT:
|
|
327
|
+
return this._handlePlacement(cmd);
|
|
326
328
|
default:
|
|
327
329
|
// TODO: Implement remaining actions when needed:
|
|
328
|
-
// - a=p (placement): place a previously transmitted image
|
|
329
330
|
// - a=f (frame): animation frame operations
|
|
330
331
|
// - a=a (animation): animation control
|
|
331
332
|
// - a=c (compose): compose images
|
|
@@ -357,9 +358,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
357
358
|
return this._handleTransmitDisplay(cmd, bytes, decodeError);
|
|
358
359
|
case KittyAction.QUERY:
|
|
359
360
|
return this._handleQuery(cmd, bytes, decodeError);
|
|
361
|
+
case KittyAction.PLACEMENT:
|
|
362
|
+
// a=p ignores any payload — image data was already transmitted
|
|
363
|
+
return this._handlePlacement(cmd);
|
|
360
364
|
default:
|
|
361
365
|
// TODO: Implement remaining actions when needed:
|
|
362
|
-
// - a=p (placement): place a previously transmitted image
|
|
363
366
|
// - a=f (frame): animation frame operations
|
|
364
367
|
// - a=a (animation): animation control
|
|
365
368
|
// - a=c (compose): compose images
|
|
@@ -370,6 +373,23 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
370
373
|
}
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
private _handlePlacement(cmd: IKittyCommand): boolean | Promise<boolean> {
|
|
377
|
+
if (cmd.id === undefined) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
const id = cmd.id;
|
|
381
|
+
const image = this._kittyStorage.getImage(id);
|
|
382
|
+
if (!image) {
|
|
383
|
+
this._sendResponse(id, 'ENOENT:image not found', cmd.quiet ?? 0, cmd.placementId);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const result = this._displayImage(image, cmd);
|
|
387
|
+
return result.then(success => {
|
|
388
|
+
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0, cmd.placementId);
|
|
389
|
+
return true;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
373
393
|
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
|
374
394
|
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
|
|
375
395
|
// Currently only supports direct transmission (t=d, the default).
|
|
@@ -492,6 +512,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
492
512
|
break;
|
|
493
513
|
case 'i':
|
|
494
514
|
case 'I':
|
|
515
|
+
// TODO: When placement id tracking is implemented (see TODO in
|
|
516
|
+
// KittyImageStorage), d=i with p=<pid> should delete only that
|
|
517
|
+
// specific placement, while d=i without p should delete all
|
|
518
|
+
// placements for the image.
|
|
495
519
|
if (cmd.id !== undefined) {
|
|
496
520
|
const pending = this._pendingTransmissions.get(cmd.id);
|
|
497
521
|
if (pending) {
|
|
@@ -508,12 +532,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
508
532
|
return true;
|
|
509
533
|
}
|
|
510
534
|
|
|
511
|
-
private _sendResponse(id: number, message: string, quiet: number): void {
|
|
535
|
+
private _sendResponse(id: number, message: string, quiet: number, placementId?: number): void {
|
|
512
536
|
const isOk = message === 'OK';
|
|
513
537
|
if (isOk && quiet === 1) return;
|
|
514
538
|
if (!isOk && quiet === 2) return;
|
|
515
539
|
|
|
516
|
-
const
|
|
540
|
+
const pPart = placementId ? `,p=${placementId}` : '';
|
|
541
|
+
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
|
|
517
542
|
this._coreTerminal._core.coreService.triggerDataEvent(response);
|
|
518
543
|
}
|
|
519
544
|
|
|
@@ -526,115 +551,137 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|
|
526
551
|
}
|
|
527
552
|
|
|
528
553
|
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
|
529
|
-
let bitmap = await this._createBitmap(image);
|
|
530
|
-
|
|
531
|
-
const cropX = Math.max(0, cmd.x ?? 0);
|
|
532
|
-
const cropY = Math.max(0, cmd.y ?? 0);
|
|
533
|
-
const cropW = cmd.sourceWidth || (bitmap.width - cropX);
|
|
534
|
-
const cropH = cmd.sourceHeight || (bitmap.height - cropY);
|
|
554
|
+
let bitmap: ImageBitmap | undefined = await this._createBitmap(image);
|
|
535
555
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
bitmap.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
|
553
|
-
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
|
554
|
-
|
|
555
|
-
// Per spec: c/r default to image's natural cell dimensions
|
|
556
|
-
let imgCols = cmd.columns ?? Math.ceil(bitmap.width / cw);
|
|
557
|
-
let imgRows = cmd.rows ?? Math.ceil(bitmap.height / ch);
|
|
558
|
-
|
|
559
|
-
let w = bitmap.width;
|
|
560
|
-
let h = bitmap.height;
|
|
561
|
-
|
|
562
|
-
// Scale bitmap to fit placement rectangle when c/r are specified
|
|
563
|
-
if (cmd.columns !== undefined || cmd.rows !== undefined) {
|
|
564
|
-
w = Math.round(imgCols * cw);
|
|
565
|
-
h = Math.round(imgRows * ch);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (w * h > this._opts.pixelLimit) {
|
|
569
|
-
bitmap.close();
|
|
570
|
-
throw new Error('image exceeds pixel limit');
|
|
571
|
-
}
|
|
556
|
+
try {
|
|
557
|
+
const cropX = Math.max(0, cmd.x ?? 0);
|
|
558
|
+
const cropY = Math.max(0, cmd.y ?? 0);
|
|
559
|
+
const cropW = cmd.sourceWidth || (bitmap.width - cropX);
|
|
560
|
+
const cropH = cmd.sourceHeight || (bitmap.height - cropY);
|
|
561
|
+
|
|
562
|
+
const maxCropW = Math.max(0, bitmap.width - cropX);
|
|
563
|
+
const maxCropH = Math.max(0, bitmap.height - cropY);
|
|
564
|
+
const finalCropW = Math.max(0, Math.min(cropW, maxCropW));
|
|
565
|
+
const finalCropH = Math.max(0, Math.min(cropH, maxCropH));
|
|
566
|
+
|
|
567
|
+
if (finalCropW === 0 || finalCropH === 0) {
|
|
568
|
+
throw new Error('invalid source rectangle');
|
|
569
|
+
}
|
|
572
570
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
571
|
+
if (cropX !== 0 || cropY !== 0 || finalCropW !== bitmap.width || finalCropH !== bitmap.height) {
|
|
572
|
+
const cropped = await createImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH);
|
|
573
|
+
bitmap.close();
|
|
574
|
+
bitmap = cropped;
|
|
575
|
+
}
|
|
578
576
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
577
|
+
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
|
578
|
+
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
|
579
|
+
|
|
580
|
+
// Per spec: c/r default to image's natural cell dimensions.
|
|
581
|
+
// If only one of c/r is specified, compute the other from image aspect ratio.
|
|
582
|
+
let imgCols: number;
|
|
583
|
+
let imgRows: number;
|
|
584
|
+
if (cmd.columns !== undefined && cmd.rows !== undefined) {
|
|
585
|
+
imgCols = cmd.columns;
|
|
586
|
+
imgRows = cmd.rows;
|
|
587
|
+
} else if (cmd.columns !== undefined) {
|
|
588
|
+
imgCols = cmd.columns;
|
|
589
|
+
imgRows = Math.max(1, Math.ceil((bitmap.height / bitmap.width) * (imgCols * cw) / ch));
|
|
590
|
+
} else if (cmd.rows !== undefined) {
|
|
591
|
+
imgRows = cmd.rows;
|
|
592
|
+
imgCols = Math.max(1, Math.ceil((bitmap.width / bitmap.height) * (imgRows * ch) / cw));
|
|
593
|
+
} else {
|
|
594
|
+
imgCols = Math.ceil(bitmap.width / cw);
|
|
595
|
+
imgRows = Math.ceil(bitmap.height / ch);
|
|
596
|
+
}
|
|
585
597
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
finalBitmap = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
|
589
|
-
bitmap.close();
|
|
590
|
-
}
|
|
598
|
+
let w = bitmap.width;
|
|
599
|
+
let h = bitmap.height;
|
|
591
600
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const offsetCanvas = ImageRenderer.createCanvas(window.document, finalBitmap.width + xOffset, finalBitmap.height + yOffset);
|
|
597
|
-
const offsetCtx = offsetCanvas.getContext('2d');
|
|
598
|
-
if (!offsetCtx) {
|
|
599
|
-
finalBitmap.close();
|
|
600
|
-
throw new Error('Failed to create offset canvas context');
|
|
601
|
+
// Scale bitmap to fit placement rectangle when c/r are specified
|
|
602
|
+
if (cmd.columns !== undefined || cmd.rows !== undefined) {
|
|
603
|
+
w = Math.round(imgCols * cw);
|
|
604
|
+
h = Math.round(imgRows * ch);
|
|
601
605
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const offsetBitmap = await createImageBitmap(offsetCanvas);
|
|
605
|
-
offsetCanvas.width = offsetCanvas.height = 0;
|
|
606
|
-
finalBitmap.close();
|
|
607
|
-
finalBitmap = offsetBitmap;
|
|
608
|
-
w = finalBitmap.width;
|
|
609
|
-
h = finalBitmap.height;
|
|
606
|
+
|
|
610
607
|
if (w * h > this._opts.pixelLimit) {
|
|
611
|
-
finalBitmap.close();
|
|
612
608
|
throw new Error('image exceeds pixel limit');
|
|
613
609
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
610
|
+
|
|
611
|
+
// Save cursor position before addImage modifies it
|
|
612
|
+
const buffer = this._coreTerminal._core.buffer;
|
|
613
|
+
const savedX = buffer.x;
|
|
614
|
+
const savedY = buffer.y;
|
|
615
|
+
const savedYbase = buffer.ybase;
|
|
616
|
+
|
|
617
|
+
// Determine layer based on z-index: negative = behind text, 0+ = on top.
|
|
618
|
+
// When z<0 we always use the bottom layer even without allowTransparency —
|
|
619
|
+
// the image will simply be hidden behind the opaque text background, which
|
|
620
|
+
// is the correct behavior (client asked for "behind text").
|
|
621
|
+
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
|
|
622
|
+
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
|
|
623
|
+
|
|
624
|
+
if (w !== bitmap.width || h !== bitmap.height) {
|
|
625
|
+
const scaled = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
|
626
|
+
bitmap.close();
|
|
627
|
+
bitmap = scaled;
|
|
619
628
|
}
|
|
620
|
-
}
|
|
621
629
|
|
|
622
|
-
|
|
623
|
-
|
|
630
|
+
// Per spec: X/Y are pixel offsets within the first cell, so clamp to cell dimensions
|
|
631
|
+
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
|
|
632
|
+
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
|
|
633
|
+
if (xOffset !== 0 || yOffset !== 0) {
|
|
634
|
+
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
|
|
635
|
+
// total placement area remains c*cw × r*ch pixels and the offset image
|
|
636
|
+
// is clipped to fit. When c/r are unset, the padded canvas determines
|
|
637
|
+
// the natural cell dimensions.
|
|
638
|
+
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
|
639
|
+
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
|
640
|
+
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
|
|
641
|
+
const offsetCtx = offsetCanvas.getContext('2d');
|
|
642
|
+
if (!offsetCtx) {
|
|
643
|
+
throw new Error('Failed to create offset canvas context');
|
|
644
|
+
}
|
|
645
|
+
offsetCtx.drawImage(bitmap, xOffset, yOffset);
|
|
646
|
+
|
|
647
|
+
const offsetBitmap = await createImageBitmap(offsetCanvas);
|
|
648
|
+
offsetCanvas.width = offsetCanvas.height = 0;
|
|
649
|
+
bitmap.close();
|
|
650
|
+
bitmap = offsetBitmap;
|
|
651
|
+
w = bitmap.width;
|
|
652
|
+
h = bitmap.height;
|
|
653
|
+
if (w * h > this._opts.pixelLimit) {
|
|
654
|
+
throw new Error('image exceeds pixel limit');
|
|
655
|
+
}
|
|
656
|
+
if (cmd.columns === undefined) {
|
|
657
|
+
imgCols = Math.ceil(bitmap.width / cw);
|
|
658
|
+
}
|
|
659
|
+
if (cmd.rows === undefined) {
|
|
660
|
+
imgRows = Math.ceil(bitmap.height / ch);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
624
663
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
664
|
+
const zIndex = cmd.zIndex ?? 0;
|
|
665
|
+
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
|
|
666
|
+
bitmap = undefined; // ownership transferred to storage
|
|
667
|
+
|
|
668
|
+
// Kitty cursor movement
|
|
669
|
+
// Per spec: cursor placed at first column after last image column,
|
|
670
|
+
// on the last row of the image. C=1 means don't move cursor.
|
|
671
|
+
if (cmd.cursorMovement === 1) {
|
|
672
|
+
// C=1: restore cursor to position before image was placed
|
|
673
|
+
const scrolled = buffer.ybase - savedYbase;
|
|
674
|
+
buffer.x = savedX;
|
|
675
|
+
// Can't restore cursor to scrollback?
|
|
676
|
+
buffer.y = Math.max(savedY - scrolled, 0);
|
|
677
|
+
} else {
|
|
678
|
+
// Default (C=0): advance cursor horizontally past the image
|
|
679
|
+
// addImage already positioned cursor on the last row via lineFeeds
|
|
680
|
+
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
|
|
681
|
+
}
|
|
682
|
+
} catch (e) {
|
|
683
|
+
bitmap?.close();
|
|
684
|
+
throw e;
|
|
638
685
|
}
|
|
639
686
|
}
|
|
640
687
|
|
|
@@ -20,6 +20,15 @@ export class KittyImageStorage implements IDisposable {
|
|
|
20
20
|
|
|
21
21
|
private _nextImageId = 1;
|
|
22
22
|
private readonly _images: Map<number, IKittyImageData> = new Map();
|
|
23
|
+
// TODO: Support multiple placements per image. The kitty spec identifies
|
|
24
|
+
// placements by an (image id, placement id) pair — same i + different p
|
|
25
|
+
// values should coexist, and same i + same p should replace the prior
|
|
26
|
+
// placement. Currently we track only one storage entry per kitty image id,
|
|
27
|
+
// so multiple placements of the same image overwrite each other. Fixing
|
|
28
|
+
// this requires changing these maps to Map<number, Map<number, number>>
|
|
29
|
+
// (kittyId → placementId → storageId) and updating addImage/deleteById
|
|
30
|
+
// accordingly. The underlying shared ImageStorage would also need to
|
|
31
|
+
// support multiple entries per logical image.
|
|
23
32
|
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
|
|
24
33
|
private readonly _storageIdToKittyId: Map<number, number> = new Map();
|
|
25
34
|
|
|
@@ -81,6 +90,14 @@ export class KittyImageStorage implements IDisposable {
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
|
|
93
|
+
// Clean up stale reverse-mapping from a previous placement of the same
|
|
94
|
+
// kitty image. The old shared-storage entry is kept (it may still be
|
|
95
|
+
// visible on screen) but its reverse mapping is removed so that eviction
|
|
96
|
+
// of the old entry won't incorrectly delete the kitty image data.
|
|
97
|
+
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
|
|
98
|
+
if (oldStorageId !== undefined) {
|
|
99
|
+
this._storageIdToKittyId.delete(oldStorageId);
|
|
100
|
+
}
|
|
84
101
|
const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
|
|
85
102
|
this._kittyIdToStorageId.set(kittyId, storageId);
|
|
86
103
|
this._storageIdToKittyId.set(storageId, kittyId);
|