Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9c38786bb | ||
|
|
039e103ed8 | ||
|
|
c9aabf9cf5 | ||
|
|
dc1fab5d5d | ||
|
|
b7ef9fa25e |
@@ -3,13 +3,13 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
|
||||||
import { IIPImageStorage } from './IIPImageStorage';
|
import { IIPImageStorage } from './IIPImageStorage';
|
||||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||||
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
||||||
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
||||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
import { imageType, UNSUPPORTED_TYPE } from './Metrics';
|
||||||
|
import { Drawable } from './Primitives';
|
||||||
|
|
||||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||||
const enum DecoderConst {
|
const enum DecoderConst {
|
||||||
@@ -44,7 +44,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _opts: IImageAddonOptions,
|
private readonly _opts: IImageAddonOptions,
|
||||||
private readonly _renderer: ImageRenderer,
|
|
||||||
private readonly _storage: IIPImageStorage,
|
private readonly _storage: IIPImageStorage,
|
||||||
private readonly _coreTerminal: ITerminalExt
|
private readonly _coreTerminal: ITerminalExt
|
||||||
) {
|
) {
|
||||||
@@ -118,11 +117,13 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
if (seqType === SequenceType.REPORTCELLSIZE) {
|
if (seqType === SequenceType.REPORTCELLSIZE) {
|
||||||
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
||||||
|
// IMPORTANT: ReportCellSize uses logical points (CSS pixels)
|
||||||
let w = CELL_SIZE_DEFAULT.width;
|
let w = CELL_SIZE_DEFAULT.width;
|
||||||
let h = CELL_SIZE_DEFAULT.height;
|
let h = CELL_SIZE_DEFAULT.height;
|
||||||
if (this._renderer.dimensions) {
|
const dimensions = this._coreTerminal.dimensions;
|
||||||
w = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
if (dimensions) {
|
||||||
h = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
w = dimensions.css.canvas.width / this._coreTerminal.cols;
|
||||||
|
h = dimensions.css.canvas.height / this._coreTerminal.rows;
|
||||||
}
|
}
|
||||||
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
||||||
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
||||||
@@ -147,9 +148,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
||||||
|
|
||||||
let w = 0;
|
|
||||||
let h = 0;
|
|
||||||
|
|
||||||
// early exit condition chain
|
// early exit condition chain
|
||||||
let cond: number | boolean;
|
let cond: number | boolean;
|
||||||
let metrics = UNSUPPORTED_TYPE;
|
let metrics = UNSUPPORTED_TYPE;
|
||||||
@@ -157,12 +155,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
if (cond = !this._dec.end()) {
|
if (cond = !this._dec.end()) {
|
||||||
metrics = imageType(this._dec.data8);
|
metrics = imageType(this._dec.data8);
|
||||||
if (cond = metrics.mime !== 'unsupported') {
|
if (cond = metrics.mime !== 'unsupported') {
|
||||||
w = metrics.width;
|
if (!(cond = metrics.width && metrics.height && metrics.width * metrics.height < this._opts.pixelLimit)) {
|
||||||
h = metrics.height;
|
|
||||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
|
||||||
[w, h] = this._resize(w, h).map(Math.floor);
|
|
||||||
cond = w && h && w * h < this._opts.pixelLimit;
|
|
||||||
} else {
|
|
||||||
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
|
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -177,30 +170,24 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let blob: Blob | ImageData;
|
let bmSrc: Blob | ImageData;
|
||||||
|
let imgBlob: Blob;
|
||||||
if (metrics.mime === 'image/qoi') {
|
if (metrics.mime === 'image/qoi') {
|
||||||
const data = this._qoiDec.decode(this._dec.data8);
|
const data = this._qoiDec.decode(this._dec.data8);
|
||||||
blob = new ImageData(
|
bmSrc = new ImageData(
|
||||||
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
||||||
this._qoiDec.width,
|
this._qoiDec.width,
|
||||||
this._qoiDec.height
|
this._qoiDec.height
|
||||||
);
|
);
|
||||||
this._qoiDec.release();
|
this._qoiDec.release();
|
||||||
if (w === this._qoiDec.width && h === this._qoiDec.height) {
|
|
||||||
// use fast-path if we don't need to rescale
|
|
||||||
this._dec.release();
|
|
||||||
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
|
|
||||||
canvas.getContext('2d')?.putImageData(blob, 0, 0);
|
|
||||||
this._storage.addImage(canvas);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
blob = new Blob([this._dec.data8], { type: metrics.mime });
|
imgBlob = bmSrc = new Blob([this._dec.data8], { type: metrics.mime });
|
||||||
}
|
}
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
return createImageBitmap(bmSrc)
|
||||||
.then(bm => {
|
.then(bm => {
|
||||||
this._storage.addImage(bm);
|
const [w, h] = this._resize(metrics.width, metrics.height);
|
||||||
|
this._storage.addImage(new Drawable(bm), imgBlob, metrics, w / metrics.width, h / metrics.height);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
@@ -210,13 +197,25 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _resize(w: number, h: number): [number, number] {
|
private _resize(w: number, h: number): [number, number] {
|
||||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
let cw;
|
||||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
let ch;
|
||||||
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
|
let width;
|
||||||
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
|
let height;
|
||||||
|
const dimensions = this._coreTerminal.dimensions;
|
||||||
|
if (dimensions) {
|
||||||
|
width = dimensions.device.canvas.width;
|
||||||
|
height = dimensions.device.canvas.height;
|
||||||
|
cw = width / this._coreTerminal.cols;
|
||||||
|
ch = height / this._coreTerminal.rows;
|
||||||
|
} else {
|
||||||
|
cw = CELL_SIZE_DEFAULT.width;
|
||||||
|
ch = CELL_SIZE_DEFAULT.height;
|
||||||
|
width = cw * this._coreTerminal.cols;
|
||||||
|
height = ch * this._coreTerminal.rows;
|
||||||
|
}
|
||||||
|
|
||||||
const rw = this._dim(this._header.width!, width, cw);
|
const rw = this._dim(this._header.width ?? '', width, cw);
|
||||||
const rh = this._dim(this._header.height!, height, ch);
|
const rh = this._dim(this._header.height ?? '', height, ch);
|
||||||
if (!rw && !rh) {
|
if (!rw && !rh) {
|
||||||
const wf = width / w; // TODO: should this respect initial cursor offset?
|
const wf = width / w; // TODO: should this respect initial cursor offset?
|
||||||
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IAddImageOpts } from './Types';
|
import { IAddImageOpts, IDrawable, IMetrics } from './Types';
|
||||||
import { ImageStorage } from './ImageStorage';
|
import { ImageStorage } from './ImageStorage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +22,9 @@ export class IIPImageStorage {
|
|||||||
* Add an IIP image to storage.
|
* Add an IIP image to storage.
|
||||||
* Always uses scrolling mode — cursor advances past the image.
|
* Always uses scrolling mode — cursor advances past the image.
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, prescaleX: number, prescaleY: number): void {
|
||||||
this._storage.addImage(img, this._addImageOpts);
|
this._addImageOpts.prescaleX = prescaleX;
|
||||||
|
this._addImageOpts.prescaleY = prescaleY;
|
||||||
|
this._storage.addImage(src, data, metrics, this._addImageOpts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
terminal.options.windowOptions = windowOps;
|
terminal.options.windowOptions = windowOps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply placeholder setting
|
||||||
|
this._renderer.showPlaceholder(this._opts.showPlaceholder);
|
||||||
|
|
||||||
this._disposeLater(
|
this._disposeLater(
|
||||||
this._renderer,
|
this._renderer,
|
||||||
this._storage,
|
this._storage,
|
||||||
@@ -187,7 +190,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
// iTerm IIP handler
|
// iTerm IIP handler
|
||||||
if (this._opts.iipSupport) {
|
if (this._opts.iipSupport) {
|
||||||
const iipStorage = new IIPImageStorage(this._storage!);
|
const iipStorage = new IIPImageStorage(this._storage!);
|
||||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
const iipHandler = new IIPHandler(this._opts, iipStorage, terminal);
|
||||||
this._handlers.set('iip', iipHandler);
|
this._handlers.set('iip', iipHandler);
|
||||||
this._disposeLater(
|
this._disposeLater(
|
||||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||||
@@ -250,10 +253,6 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
return this._storage?.getImageAtBufferCell(x, y);
|
return this._storage?.getImageAtBufferCell(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
||||||
return this._storage?.extractTileAtBufferCell(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _report(s: string): void {
|
private _report(s: string): void {
|
||||||
this._terminal?._core.input(s, false);
|
this._terminal?._core.input(s, false);
|
||||||
}
|
}
|
||||||
@@ -344,8 +343,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
switch (params[1]) {
|
switch (params[1]) {
|
||||||
// we only implement read and read_max here
|
// we only implement read and read_max here
|
||||||
case GaAction.READ:
|
case GaAction.READ:
|
||||||
let width = this._renderer?.dimensions?.css.canvas.width;
|
let width = this._renderer?.dimensions?.device.canvas.width;
|
||||||
let height = this._renderer?.dimensions?.css.canvas.height;
|
let height = this._renderer?.dimensions?.device.canvas.height;
|
||||||
if (!width || !height) {
|
if (!width || !height) {
|
||||||
// for some reason we have no working image renderer
|
// for some reason we have no working image renderer
|
||||||
// --> fallback to default cell size
|
// --> fallback to default cell size
|
||||||
|
|||||||
@@ -3,19 +3,14 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
|
||||||
import { IDisposable } from '@xterm/xterm';
|
import { IDisposable } from '@xterm/xterm';
|
||||||
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
import type { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||||
|
import { createCanvas } from './Primitives';
|
||||||
|
|
||||||
const enum Constants {
|
|
||||||
PLACEHOLDER_LENGTH = 4096,
|
|
||||||
PLACEHOLDER_HEIGHT = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageRenderer - terminal frontend extension:
|
* ImageRenderer - terminal frontend extension:
|
||||||
* - provide primitives for canvas, ImageData, Bitmap (static)
|
|
||||||
* - add canvas layer to DOM (browser only for now)
|
* - add canvas layer to DOM (browser only for now)
|
||||||
* - draw image tiles onRender
|
* - draw image tiles onRender
|
||||||
*/
|
*/
|
||||||
@@ -24,51 +19,14 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
||||||
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
||||||
private _placeholder: HTMLCanvasElement | undefined;
|
private _placeholder: HTMLCanvasElement | undefined;
|
||||||
private _placeholderBitmap: ImageBitmap | undefined;
|
|
||||||
private _optionsRefresh = this._register(new MutableDisposable());
|
private _optionsRefresh = this._register(new MutableDisposable());
|
||||||
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
||||||
private _renderService: IRenderService | undefined;
|
private _renderService: IRenderService | undefined;
|
||||||
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
||||||
|
|
||||||
// drawing primitive - canvas
|
// some local variables for faster access
|
||||||
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
|
private _dimensions: IRenderDimensions | undefined;
|
||||||
/**
|
private _cellSize: ICellSize | undefined;
|
||||||
* NOTE: We normally dont care, from which document the canvas
|
|
||||||
* gets created, so we can fall back to global document,
|
|
||||||
* if the terminal has no document associated yet.
|
|
||||||
* This way early image loads before calling .open keep working
|
|
||||||
* (still discouraged though, as the metrics will be screwed up).
|
|
||||||
* Only the DOM output canvas should be on the terminal's document,
|
|
||||||
* which gets explicitly checked in `insertLayerToDom`.
|
|
||||||
*/
|
|
||||||
const canvas = (localDocument ?? document).createElement('canvas');
|
|
||||||
canvas.width = width | 0;
|
|
||||||
canvas.height = height | 0;
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawing primitive - ImageData with optional buffer
|
|
||||||
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
|
|
||||||
if (typeof ImageData !== 'function') {
|
|
||||||
const imgData = ctx.createImageData(width, height);
|
|
||||||
if (buffer) {
|
|
||||||
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
|
|
||||||
}
|
|
||||||
return imgData;
|
|
||||||
}
|
|
||||||
return buffer
|
|
||||||
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
|
|
||||||
: new ImageData(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawing primitive - ImageBitmap
|
|
||||||
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
|
|
||||||
if (typeof createImageBitmap !== 'function') {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
return createImageBitmap(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
constructor(private _terminal: ITerminalExt) {
|
constructor(private _terminal: ITerminalExt) {
|
||||||
super();
|
super();
|
||||||
@@ -100,8 +58,6 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
}
|
}
|
||||||
this._renderService = undefined;
|
this._renderService = undefined;
|
||||||
this._layers.clear();
|
this._layers.clear();
|
||||||
this._placeholderBitmap?.close();
|
|
||||||
this._placeholderBitmap = undefined;
|
|
||||||
this._placeholder = undefined;
|
this._placeholder = undefined;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -110,15 +66,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
* Enable the placeholder.
|
* Enable the placeholder.
|
||||||
*/
|
*/
|
||||||
public showPlaceholder(value: boolean): void {
|
public showPlaceholder(value: boolean): void {
|
||||||
if (value) {
|
this._placeholder = value ? this._createPlaceHolder() : undefined;
|
||||||
if (!this._placeholder && this.cellSize.height !== -1) {
|
|
||||||
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._placeholderBitmap?.close();
|
|
||||||
this._placeholderBitmap = undefined;
|
|
||||||
this._placeholder = undefined;
|
|
||||||
}
|
|
||||||
this._renderService?.refreshRows(0, this._terminal.rows);
|
this._renderService?.refreshRows(0, this._terminal.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,22 +79,27 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current cell size (float).
|
* Current cell size.
|
||||||
*/
|
*/
|
||||||
public get cellSize(): ICellSize {
|
public getCellSize(dimensions?: IRenderDimensions): ICellSize | undefined {
|
||||||
return {
|
dimensions ??= this._terminal.dimensions;
|
||||||
width: this.dimensions?.css.cell.width || -1,
|
if (dimensions) {
|
||||||
height: this.dimensions?.css.cell.height || -1
|
return {
|
||||||
};
|
width: dimensions.device.canvas.width / this._terminal.cols,
|
||||||
|
height: dimensions.device.canvas.height / this._terminal.rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear a region of the image layer canvas.
|
* Clear a region of the image layer canvas.
|
||||||
*/
|
*/
|
||||||
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
||||||
const y = start * (this.dimensions?.css.cell.height || 0);
|
const deviceGrid = this._cellSize;
|
||||||
const w = this.dimensions?.css.canvas.width || 0;
|
if (!deviceGrid || !this._dimensions) return;
|
||||||
const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
|
const y = Math.floor(start * deviceGrid.height);
|
||||||
|
const w = this._dimensions.device.canvas.width;
|
||||||
|
const h = Math.ceil((end + 1 - start) * deviceGrid.height);
|
||||||
if (!layer || layer === 'top') {
|
if (!layer || layer === 'top') {
|
||||||
this._layers.get('top')?.clearRect(0, y, w, h);
|
this._layers.get('top')?.clearRect(0, y, w, h);
|
||||||
}
|
}
|
||||||
@@ -174,142 +127,103 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
*/
|
*/
|
||||||
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
||||||
const ctx = this._layers.get(imgSpec.layer);
|
const ctx = this._layers.get(imgSpec.layer);
|
||||||
if (!ctx) {
|
const initialGrid = imgSpec.cellSize;
|
||||||
return;
|
const deviceGrid = this._cellSize;
|
||||||
}
|
|
||||||
const { width, height } = this.cellSize;
|
|
||||||
|
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
if (!ctx || !imgSpec.src.native || !deviceGrid) {
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._rescaleImage(imgSpec, width, height);
|
const img = imgSpec.src;
|
||||||
const img = imgSpec.actual!;
|
const cols = Math.ceil(img.width * imgSpec.prescaleX / initialGrid.width);
|
||||||
const cols = Math.ceil(img.width / width);
|
|
||||||
|
|
||||||
const sx = (tileId % cols) * width;
|
const gridWidth = initialGrid.width / imgSpec.prescaleX;
|
||||||
const sy = Math.floor(tileId / cols) * height;
|
const gridHeight = initialGrid.height / imgSpec.prescaleY;
|
||||||
const dx = col * width;
|
|
||||||
const dy = row * height;
|
|
||||||
|
|
||||||
// safari bug: never access image source out of bounds
|
// crops are initialGrid px
|
||||||
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
|
const sx = (tileId % cols) * gridWidth + imgSpec.precropX;
|
||||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
const sy = Math.floor(tileId / cols) * gridHeight + imgSpec.precropY;
|
||||||
|
const dx = Math.floor(col * deviceGrid.width + imgSpec.offsetX);
|
||||||
|
const dy = Math.floor(row * deviceGrid.height + imgSpec.offsetY);
|
||||||
|
|
||||||
|
// safari bug: never access image source out of bounds, thus we clamp its dimensions
|
||||||
|
const sWidth = Math.min(count * gridWidth, img.width - sx);
|
||||||
|
const sHeight = Math.min(gridHeight, img.height - sy);
|
||||||
|
const dWidth = Math.ceil(sWidth / gridWidth * deviceGrid.width * imgSpec.scaleX);
|
||||||
|
const dHeight = Math.ceil(sHeight / gridHeight * deviceGrid.height * imgSpec.scaleY);
|
||||||
|
|
||||||
|
if (imgSpec.blendMode === 'overwrite') {
|
||||||
|
ctx.clearRect(dx, dy, dWidth, dHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
||||||
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
||||||
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
||||||
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
img,
|
img.native,
|
||||||
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
Math.floor(sx), Math.floor(sy), Math.ceil(sWidth), Math.ceil(sHeight),
|
||||||
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
dx, dy, dWidth, dHeight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a single tile from an image.
|
|
||||||
*/
|
|
||||||
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
|
|
||||||
const { width, height } = this.cellSize;
|
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._rescaleImage(imgSpec, width, height);
|
|
||||||
const img = imgSpec.actual!;
|
|
||||||
const cols = Math.ceil(img.width / width);
|
|
||||||
const sx = (tileId % cols) * width;
|
|
||||||
const sy = Math.floor(tileId / cols) * height;
|
|
||||||
const finalWidth = width + sx > img.width ? img.width - sx : width;
|
|
||||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
||||||
|
|
||||||
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(
|
|
||||||
img,
|
|
||||||
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
|
|
||||||
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
|
|
||||||
);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a line with placeholder on the image layer canvas.
|
* Draw a line with placeholder on the image layer canvas.
|
||||||
*/
|
*/
|
||||||
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
||||||
const ctx = this._layers.get('top');
|
const ctx = this._layers.get('top');
|
||||||
if (ctx) {
|
const deviceGrid = this._cellSize;
|
||||||
const { width, height } = this.cellSize;
|
if (!ctx || !deviceGrid) {
|
||||||
|
return;
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._placeholder) {
|
|
||||||
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
||||||
} else if (height >= this._placeholder!.height) {
|
|
||||||
this._createPlaceHolder(height + 1);
|
|
||||||
}
|
|
||||||
if (!this._placeholder) return;
|
|
||||||
ctx.drawImage(
|
|
||||||
this._placeholderBitmap ?? this._placeholder!,
|
|
||||||
col * width,
|
|
||||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
||||||
width * count,
|
|
||||||
height,
|
|
||||||
col * width,
|
|
||||||
row * height,
|
|
||||||
width * count,
|
|
||||||
height
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
this._placeholder ??= this._createPlaceHolder();
|
||||||
|
if (!this._placeholder) return;
|
||||||
|
ctx.drawImage(
|
||||||
|
this._placeholder,
|
||||||
|
0, 0, 1, 1,
|
||||||
|
col * deviceGrid.width,
|
||||||
|
row * deviceGrid.height,
|
||||||
|
count * deviceGrid.width,
|
||||||
|
deviceGrid.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rescale image layer canvas if needed.
|
* Rescale image layer canvas if needed.
|
||||||
* Checked once from `ImageStorage.render`.
|
* Checked once from `ImageStorage.render`.
|
||||||
|
* NOTE: This method updates dimensions on instance properties
|
||||||
|
* for faster access during draw and clear calls.
|
||||||
|
* So make sure to always call this before doing any draws.
|
||||||
*/
|
*/
|
||||||
public rescaleCanvas(): void {
|
public rescaleCanvas(force: boolean = false): void {
|
||||||
const w = this.dimensions?.css.canvas.width || 0;
|
const dimensions = this._terminal.dimensions;
|
||||||
const h = this.dimensions?.css.canvas.height || 0;
|
if (dimensions) {
|
||||||
for (const ctx of this._layers.values()) {
|
const cssW = dimensions.css.canvas.width;
|
||||||
if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
|
const cssH = dimensions.css.canvas.height;
|
||||||
ctx.canvas.width = w;
|
const devW = dimensions.device.canvas.width;
|
||||||
ctx.canvas.height = h;
|
const devH = dimensions.device.canvas.height;
|
||||||
|
let recalc = force;
|
||||||
|
for (const ctx of this._layers.values()) {
|
||||||
|
if (ctx.canvas.width !== devW
|
||||||
|
|| ctx.canvas.height !== devH
|
||||||
|
|| ctx.canvas.style.width !== `${cssW}px`
|
||||||
|
|| ctx.canvas.style.height !== `${cssH}px`
|
||||||
|
) {
|
||||||
|
ctx.canvas.width = devW;
|
||||||
|
ctx.canvas.height = devH;
|
||||||
|
ctx.canvas.style.width = `${cssW}px`;
|
||||||
|
ctx.canvas.style.height = `${cssH}px`;
|
||||||
|
recalc = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (recalc) {
|
||||||
}
|
this._dimensions = dimensions;
|
||||||
|
this._cellSize = this.getCellSize(dimensions);
|
||||||
/**
|
}
|
||||||
* Rescale image in storage if needed.
|
} else {
|
||||||
*/
|
// we were not able to get render dimensions:
|
||||||
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
|
// set cellsize to undefined so we don't try to draw anything
|
||||||
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
|
this._cellSize = this.getCellSize();
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
|
|
||||||
if (currentWidth === originalWidth && currentHeight === originalHeight) {
|
|
||||||
spec.actual = spec.orig;
|
|
||||||
spec.actualCellSize.width = originalWidth;
|
|
||||||
spec.actualCellSize.height = originalHeight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const canvas = ImageRenderer.createCanvas(
|
|
||||||
this.document,
|
|
||||||
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
|
|
||||||
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
|
|
||||||
);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
|
|
||||||
spec.actual = canvas;
|
|
||||||
spec.actualCellSize.width = currentWidth;
|
|
||||||
spec.actualCellSize.height = currentHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +250,12 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
if (this._layers.has(layer)) {
|
if (this._layers.has(layer)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canvas = ImageRenderer.createCanvas(
|
const canvas = createCanvas(
|
||||||
this.document, this.dimensions?.css.canvas.width || 0,
|
this.document,
|
||||||
this.dimensions?.css.canvas.height || 0
|
this.dimensions?.device.canvas.width || 0,
|
||||||
|
this.dimensions?.device.canvas.height || 0,
|
||||||
|
this.dimensions?.css.canvas.width || 0,
|
||||||
|
this.dimensions?.css.canvas.height || 0,
|
||||||
);
|
);
|
||||||
canvas.classList.add(`xterm-image-layer-${layer}`);
|
canvas.classList.add(`xterm-image-layer-${layer}`);
|
||||||
const screenElement = this._terminal._core.screenElement;
|
const screenElement = this._terminal._core.screenElement;
|
||||||
@@ -366,6 +283,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._layers.set(layer, ctx);
|
this._layers.set(layer, ctx);
|
||||||
|
// force rescaling to update stored cellSize (maybe not be populated yet)
|
||||||
|
this.rescaleCanvas(true);
|
||||||
this.clearAll(layer);
|
this.clearAll(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,41 +300,17 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
return this._layers.has(layer);
|
return this._layers.has(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
|
/**
|
||||||
this._placeholderBitmap?.close();
|
* Create a semi-transparent gray 1x1 placeholder
|
||||||
this._placeholderBitmap = undefined;
|
*/
|
||||||
|
private _createPlaceHolder(): HTMLCanvasElement | undefined {
|
||||||
// create blueprint to fill placeholder with
|
const imgData = new ImageData(1, 1);
|
||||||
const bWidth = 32; // must be 2^n
|
new Uint32Array(imgData.data.buffer).fill(128 << 24 | 128 << 16 | 128 << 8 | 128);
|
||||||
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
|
const canvas = createCanvas(this.document, 1, 1);
|
||||||
const ctx = blueprint.getContext('2d', { alpha: false });
|
const ctx = canvas.getContext('2d', { alpha: true });
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
|
|
||||||
const d32 = new Uint32Array(imgData.data.buffer);
|
|
||||||
const black = toRGBA8888(0, 0, 0);
|
|
||||||
const white = toRGBA8888(255, 255, 255);
|
|
||||||
d32.fill(black);
|
|
||||||
for (let y = 0; y < height; ++y) {
|
|
||||||
const shift = y % 2;
|
|
||||||
const offset = y * bWidth;
|
|
||||||
for (let x = 0; x < bWidth; x += 2) {
|
|
||||||
d32[offset + x + shift] = white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.putImageData(imgData, 0, 0);
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
return canvas;
|
||||||
// create placeholder line, width aligned to blueprint width
|
|
||||||
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
|
|
||||||
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
|
||||||
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
|
||||||
if (!ctx2) {
|
|
||||||
this._placeholder = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < width; i += bWidth) {
|
|
||||||
ctx2.drawImage(blueprint, i, 0);
|
|
||||||
}
|
|
||||||
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get document(): Document | undefined {
|
public get document(): Document | undefined {
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
import { IDisposable } from '@xterm/xterm';
|
import { IDisposable } from '@xterm/xterm';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
import { ImageRenderer } from './ImageRenderer';
|
||||||
import type {
|
import type {
|
||||||
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts
|
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts,
|
||||||
|
IDrawable,
|
||||||
|
IMetrics
|
||||||
} from './Types';
|
} from './Types';
|
||||||
import type { IBufferLine } from 'common/buffer/Types';
|
import type { IBufferLine } from 'common/buffer/Types';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
import { CellData } from 'common/buffer/CellData';
|
||||||
|
import { createCanvas } from './Primitives';
|
||||||
|
|
||||||
// fallback default cell size
|
// fallback default cell size
|
||||||
export const CELL_SIZE_DEFAULT: ICellSize = {
|
export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||||
@@ -43,7 +46,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
// whether render should do a full clear
|
// whether render should do a full clear
|
||||||
private _needsFullClear = false;
|
private _needsFullClear = false;
|
||||||
// hard limit of stored pixels (fallback limit of 10 MB)
|
// hard limit of stored pixels (fallback limit of 10 MB)
|
||||||
private _pixelLimit: number = 2500000;
|
private _byteLimit: number = 10000000;
|
||||||
private _workCell: CellData = new CellData();
|
private _workCell: CellData = new CellData();
|
||||||
|
|
||||||
private _viewportMetrics: { cols: number, rows: number };
|
private _viewportMetrics: { cols: number, rows: number };
|
||||||
@@ -83,43 +86,54 @@ export class ImageStorage implements IDisposable {
|
|||||||
this._renderer.clearAll();
|
this._renderer.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: below we need to introduce separation of:
|
||||||
|
* - RAM usage
|
||||||
|
* - VRAM usage
|
||||||
|
* - Blob unsage
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
public getLimit(): number {
|
public getLimit(): number {
|
||||||
return this._pixelLimit * 4 / 1000000;
|
return this._byteLimit / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLimit(value: number): void {
|
public setLimit(value: number): void {
|
||||||
if (value < 0.5 || value > 1000) {
|
if (value < 0.5 || value > 1000) {
|
||||||
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
|
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1GB');
|
||||||
}
|
}
|
||||||
this._pixelLimit = (value / 4 * 1000000) >>> 0;
|
this._byteLimit = (value * 1000000) >>> 0;
|
||||||
this._evictOldest(0);
|
this._evictOldest(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUsage(): number {
|
public getUsage(): number {
|
||||||
return this._getStoredPixels() * 4 / 1000000;
|
return this._getVRAMUsage() / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getStoredPixels(): number {
|
private _getVRAMUsage(): number {
|
||||||
let storedPixels = 0;
|
let bytes = 0;
|
||||||
for (const spec of this._images.values()) {
|
for (const spec of this._images.values()) {
|
||||||
if (spec.orig) {
|
bytes += spec.src.bytes;
|
||||||
storedPixels += spec.orig.width * spec.orig.height;
|
}
|
||||||
if (spec.actual && spec.actual !== spec.orig) {
|
return bytes;
|
||||||
storedPixels += spec.actual.width * spec.actual.height;
|
}
|
||||||
}
|
|
||||||
|
public getBlobUsage(): number {
|
||||||
|
let size = 0;
|
||||||
|
for (const spec of this._images.values()) {
|
||||||
|
if (spec.data instanceof Blob) {
|
||||||
|
size += spec.data.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return storedPixels;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delImg(id: number): void {
|
private _delImg(id: number): void {
|
||||||
const spec = this._images.get(id);
|
const spec = this._images.get(id);
|
||||||
if (!spec) return;
|
if (!spec) return;
|
||||||
this._images.delete(id);
|
this._images.delete(id);
|
||||||
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
spec.src.close();
|
||||||
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
spec.data = undefined;
|
||||||
spec.orig.close();
|
|
||||||
}
|
|
||||||
this.onImageDeleted?.(id);
|
this.onImageDeleted?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +170,10 @@ export class ImageStorage implements IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to add an image to the storage.
|
* Method to add an drawable to the storage.
|
||||||
* @param img - The image to add (canvas or bitmap).
|
* @param src - The drawable to add.
|
||||||
|
* @param data - Blob or Uint8Array of the original image data.
|
||||||
|
* @param metrics - IMetrics of the original image.
|
||||||
* @param opts - Options for addImage:
|
* @param opts - Options for addImage:
|
||||||
* - scrolling: When true, cursor advances with the image.
|
* - scrolling: When true, cursor advances with the image.
|
||||||
* When false, image is placed at ORIGIN and cursor does not move.
|
* When false, image is placed at ORIGIN and cursor does not move.
|
||||||
@@ -166,17 +182,16 @@ export class ImageStorage implements IDisposable {
|
|||||||
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
||||||
* @returns The internal image ID assigned to the stored image.
|
* @returns The internal image ID assigned to the stored image.
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, opts: IAddImageOpts): number {
|
||||||
// never allow storage to exceed memory limit
|
// never allow storage to exceed memory limit
|
||||||
this._evictOldest(img.width * img.height);
|
this._evictOldest(src.bytes);
|
||||||
|
|
||||||
// calc rows x cols needed to display the image
|
// calc rows x cols needed to display the image
|
||||||
let cellSize = this._renderer.cellSize;
|
const prescaleX = opts.prescaleX ?? 1.0;
|
||||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
const prescaleY = opts.prescaleY ?? 1.0;
|
||||||
cellSize = CELL_SIZE_DEFAULT;
|
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
|
||||||
}
|
const cols = Math.ceil(src.width * prescaleX / cellSize.width);
|
||||||
const cols = Math.ceil(img.width / cellSize.width);
|
const rows = Math.ceil(src.height * prescaleY / cellSize.height);
|
||||||
const rows = Math.ceil(img.height / cellSize.height);
|
|
||||||
|
|
||||||
const imageId = ++this._lastId;
|
const imageId = ++this._lastId;
|
||||||
|
|
||||||
@@ -253,15 +268,24 @@ export class ImageStorage implements IDisposable {
|
|||||||
|
|
||||||
// create storage entry
|
// create storage entry
|
||||||
const imgSpec: IImageSpec = {
|
const imgSpec: IImageSpec = {
|
||||||
orig: img,
|
src,
|
||||||
origCellSize: cellSize,
|
data,
|
||||||
actual: img,
|
metrics,
|
||||||
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
cellSize,
|
||||||
marker: endMarker || undefined,
|
marker: endMarker || undefined,
|
||||||
tileCount,
|
tileCount,
|
||||||
bufferType: this._terminal.buffer.active.type,
|
bufferType: this._terminal.buffer.active.type,
|
||||||
layer: opts.layer,
|
layer: opts.layer,
|
||||||
zIndex: opts.zIndex
|
zIndex: opts.zIndex,
|
||||||
|
prescaleX,
|
||||||
|
prescaleY,
|
||||||
|
precropX: 0,
|
||||||
|
precropY: 0,
|
||||||
|
scaleX: 1.0,
|
||||||
|
scaleY: 1.0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
blendMode: 'alpha'
|
||||||
};
|
};
|
||||||
|
|
||||||
// finally add the image
|
// finally add the image
|
||||||
@@ -376,9 +400,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
}
|
}
|
||||||
col--;
|
col--;
|
||||||
if (imgSpec) {
|
if (imgSpec) {
|
||||||
if (imgSpec.actual) {
|
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
|
||||||
}
|
|
||||||
} else if (this._opts.showPlaceholder) {
|
} else if (this._opts.showPlaceholder) {
|
||||||
placeholderCalls.push({ col: startCol, row, count });
|
placeholderCalls.push({ col: startCol, row, count });
|
||||||
}
|
}
|
||||||
@@ -433,7 +455,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// found an image tile at oldCol, check if it qualifies for right exapansion
|
// found an image tile at oldCol, check if it qualifies for right exapansion
|
||||||
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
|
const tilesPerRow = Math.ceil(imgSpec.src.width * imgSpec.prescaleX / imgSpec.cellSize.width);
|
||||||
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -470,30 +492,13 @@ export class ImageStorage implements IDisposable {
|
|||||||
if (line) {
|
if (line) {
|
||||||
const e = line.getExtended(x)?.payload;
|
const e = line.getExtended(x)?.payload;
|
||||||
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
|
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
|
||||||
const orig = this._images.get(e.imageId)?.orig;
|
const src = this._images.get(e.imageId)?.src;
|
||||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
if (src?.native instanceof ImageBitmap || src?.native instanceof VideoFrame) {
|
||||||
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
const canvas = createCanvas(window.document, src.width, src.height);
|
||||||
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
|
canvas.getContext('2d')?.drawImage(src.native, 0, 0, src.width, src.height);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
return orig as HTMLCanvasElement;
|
return src?.native;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract active single tile at buffer position.
|
|
||||||
*/
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
||||||
const buffer = this._terminal._core.buffer;
|
|
||||||
const line = buffer.lines.get(y);
|
|
||||||
if (line) {
|
|
||||||
const e = line.getExtended(x)?.payload;
|
|
||||||
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
||||||
const spec = this._images.get(e.imageId);
|
|
||||||
if (spec) {
|
|
||||||
return this._renderer.extractTile(spec, e.tileId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,15 +506,12 @@ export class ImageStorage implements IDisposable {
|
|||||||
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
||||||
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
||||||
private _evictOldest(room: number): number {
|
private _evictOldest(room: number): number {
|
||||||
const used = this._getStoredPixels();
|
const used = this._getVRAMUsage();
|
||||||
let current = used;
|
let current = used;
|
||||||
while (this._pixelLimit < current + room && this._images.size) {
|
while (this._byteLimit < current + room && this._images.size) {
|
||||||
const spec = this._images.get(++this._lowestId);
|
const spec = this._images.get(++this._lowestId);
|
||||||
if (spec && spec.orig) {
|
if (spec) {
|
||||||
current -= spec.orig.width * spec.orig.height;
|
current -= spec.src.bytes;
|
||||||
if (spec.actual && spec.orig !== spec.actual) {
|
|
||||||
current -= spec.actual.width * spec.actual.height;
|
|
||||||
}
|
|
||||||
spec.marker?.dispose();
|
spec.marker?.dispose();
|
||||||
this._delImg(this._lowestId);
|
this._delImg(this._lowestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { imageType, IMetrics } from './IIPMetrics';
|
import { imageType } from './Metrics';
|
||||||
|
import type { IMetrics } from 'Types';
|
||||||
|
|
||||||
// fix missing nodejs decl
|
// fix missing nodejs decl
|
||||||
declare const require: (s: string) => any;
|
declare const require: (s: string) => any;
|
||||||
@@ -3,14 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { IMetrics } from './Types';
|
||||||
|
|
||||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'image/webp' | 'image/avif' | 'unsupported' | '';
|
|
||||||
|
|
||||||
export interface IMetrics {
|
|
||||||
mime: ImageType;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UNSUPPORTED_TYPE: IMetrics = {
|
export const UNSUPPORTED_TYPE: IMetrics = {
|
||||||
mime: 'unsupported',
|
mime: 'unsupported',
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IDrawable } from './Types';
|
||||||
|
|
||||||
|
export function createCanvas(
|
||||||
|
localDocument: Document | undefined,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
cssWidth?: number,
|
||||||
|
cssHeight?: number
|
||||||
|
): HTMLCanvasElement {
|
||||||
|
/**
|
||||||
|
* NOTE: We normally dont care, from which document the canvas
|
||||||
|
* gets created, so we can fall back to global document,
|
||||||
|
* if the terminal has no document associated yet.
|
||||||
|
* This way early image loads before calling .open keep working
|
||||||
|
* (still discouraged though, as the metrics will be screwed up).
|
||||||
|
* Only the DOM output canvas should be on the terminal's document,
|
||||||
|
* which gets explicitly checked in `insertLayerToDom`.
|
||||||
|
*/
|
||||||
|
const canvas = (localDocument ?? document).createElement('canvas');
|
||||||
|
canvas.width = width | 0;
|
||||||
|
canvas.height = height | 0;
|
||||||
|
canvas.style.width = `${(cssWidth ?? width) | 0}px`;
|
||||||
|
canvas.style.height = `${(cssHeight ?? height) | 0}px`;
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawable is a resource the ImageRenderer knows to draw on the screen.
|
||||||
|
*/
|
||||||
|
export class Drawable implements IDrawable {
|
||||||
|
public readonly width: number;
|
||||||
|
public readonly height: number;
|
||||||
|
// @readonly: only for memory tracking
|
||||||
|
public bytes: number;
|
||||||
|
|
||||||
|
constructor(public native: ImageBitmap | VideoFrame | HTMLCanvasElement) {
|
||||||
|
if (native instanceof VideoFrame) {
|
||||||
|
this.width = native.displayWidth;
|
||||||
|
this.height = native.displayHeight;
|
||||||
|
// NOTE: native.allocationSize() is too expensive
|
||||||
|
this.bytes = native.codedWidth * native.codedHeight * 4;
|
||||||
|
} else {
|
||||||
|
this.width = native.width;
|
||||||
|
this.height = native.height;
|
||||||
|
this.bytes = native.width * native.height * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
if (this.bytes === 0) return;
|
||||||
|
this.bytes = 0;
|
||||||
|
try {
|
||||||
|
if (this.native instanceof HTMLCanvasElement) {
|
||||||
|
this.native.width = 0;
|
||||||
|
this.native.height = 0;
|
||||||
|
} else {
|
||||||
|
this.native.close?.();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
this.native = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import { SixelImageStorage } from './SixelImageStorage';
|
|||||||
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
||||||
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
||||||
import { RGBA8888 } from 'sixel/lib/Types';
|
import { RGBA8888 } from 'sixel/lib/Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
|
||||||
|
|
||||||
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
|
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
|
||||||
|
import { createCanvas, Drawable } from './Primitives';
|
||||||
|
|
||||||
// always free decoder ressources after decoding if it exceeds this limit
|
// always free decoder ressources after decoding if it exceeds this limit
|
||||||
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
||||||
@@ -98,12 +98,12 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = ImageRenderer.createCanvas(undefined, width, height);
|
const canvas = createCanvas(undefined, width, height);
|
||||||
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
||||||
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
}
|
}
|
||||||
this._storage.addImage(canvas);
|
this._storage.addImage(new Drawable(canvas), undefined, { width, height, mime: 'image/sixel' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||||
import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
|
import { IImageAddonOptions, ITerminalExt, IAddImageOpts, IDrawable, IMetrics } from './Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
import { ImageRenderer } from './ImageRenderer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,9 +27,9 @@ export class SixelImageStorage {
|
|||||||
* Add a sixel image to storage.
|
* Add a sixel image to storage.
|
||||||
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics): void {
|
||||||
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
||||||
this._storage.addImage(img, this._addImageOpts);
|
this._storage.addImage(src, data, metrics, this._addImageOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,10 +39,7 @@ export class SixelImageStorage {
|
|||||||
*/
|
*/
|
||||||
public advanceCursor(height: number): void {
|
public advanceCursor(height: number): void {
|
||||||
if (this._opts.sixelScrolling) {
|
if (this._opts.sixelScrolling) {
|
||||||
let cellSize = this._renderer.cellSize;
|
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
|
||||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
|
||||||
cellSize = CELL_SIZE_DEFAULT;
|
|
||||||
}
|
|
||||||
const rows = Math.ceil(height / cellSize.height);
|
const rows = Math.ceil(height / cellSize.height);
|
||||||
for (let i = 1; i < rows; ++i) {
|
for (let i = 1; i < rows; ++i) {
|
||||||
this._terminal._core._inputHandler.lineFeed();
|
this._terminal._core._inputHandler.lineFeed();
|
||||||
|
|||||||
@@ -94,16 +94,70 @@ export interface IAddImageOpts {
|
|||||||
layer: ImageLayer;
|
layer: ImageLayer;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
cursorPos: CursorPos;
|
cursorPos: CursorPos;
|
||||||
|
prescaleX?: number;
|
||||||
|
prescaleY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDrawable {
|
||||||
|
native: ImageBitmap | VideoFrame | HTMLCanvasElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
close(): void;
|
||||||
|
readonly bytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImageType = 'unsupported'
|
||||||
|
| 'image/png'
|
||||||
|
| 'image/jpeg'
|
||||||
|
| 'image/gif'
|
||||||
|
| 'image/qoi'
|
||||||
|
| 'image/webp'
|
||||||
|
| 'image/avif'
|
||||||
|
// sixel
|
||||||
|
| 'image/sixel'
|
||||||
|
// rgb|a blobs for kitty
|
||||||
|
| 'image/rgb'
|
||||||
|
| 'image/rgba';
|
||||||
|
|
||||||
|
export interface IMetrics {
|
||||||
|
mime: ImageType;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IImageSpec {
|
export interface IImageSpec {
|
||||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
/** entries for a later IImageSource type */
|
||||||
origCellSize: ICellSize;
|
/** drawable for screen rendering */
|
||||||
actual: HTMLCanvasElement | ImageBitmap | undefined;
|
src: IDrawable;
|
||||||
actualCellSize: ICellSize;
|
/** image bytes as blob */
|
||||||
|
data: Blob | undefined;
|
||||||
|
/** metrics about image like mime and dimensions */
|
||||||
|
metrics: IMetrics;
|
||||||
|
|
||||||
|
/** entries for a later IPlacement type */
|
||||||
|
/** cell size at time of insert */
|
||||||
|
cellSize: ICellSize;
|
||||||
|
/** eviction marker */
|
||||||
marker: IMarker | undefined;
|
marker: IMarker | undefined;
|
||||||
|
/** used tiles as eviction hint */
|
||||||
tileCount: number;
|
tileCount: number;
|
||||||
|
/** buffer it was placed on */
|
||||||
bufferType: 'alternate' | 'normal';
|
bufferType: 'alternate' | 'normal';
|
||||||
|
/** layer it was placed on */
|
||||||
layer: ImageLayer;
|
layer: ImageLayer;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
|
/** scaling factor on source dimensions, default = 1.0 */
|
||||||
|
prescaleX: number;
|
||||||
|
prescaleY: number;
|
||||||
|
/** crop offset into source, default = 0 (currently unused) */
|
||||||
|
precropX: number;
|
||||||
|
precropY: number;
|
||||||
|
/** output scaling, default = 1.0 (currently unused) */
|
||||||
|
scaleX: number;
|
||||||
|
scaleY: number;
|
||||||
|
/** output offset, default = 0 (currently unused) */
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
/** compositing mode on output, default = 'alpha' (currently unused) */
|
||||||
|
blendMode: 'overwrite' | 'alpha';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
KittyPixelConstants,
|
KittyPixelConstants,
|
||||||
parseKittyCommand
|
parseKittyCommand
|
||||||
} from './KittyGraphicsTypes';
|
} from './KittyGraphicsTypes';
|
||||||
|
import { createCanvas } from '../Primitives';
|
||||||
|
|
||||||
const enum Constants {
|
const enum Constants {
|
||||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||||
@@ -573,8 +574,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
bitmap = cropped;
|
bitmap = cropped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
const cw = this._renderer.dimensions?.device.cell.width || CELL_SIZE_DEFAULT.width;
|
||||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
const ch = this._renderer.dimensions?.device.cell.height || CELL_SIZE_DEFAULT.height;
|
||||||
|
|
||||||
// Per spec: c/r default to image's natural cell dimensions.
|
// Per spec: c/r default to image's natural cell dimensions.
|
||||||
// If only one of c/r is specified, compute the other from image aspect ratio.
|
// If only one of c/r is specified, compute the other from image aspect ratio.
|
||||||
@@ -636,7 +637,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
// the natural cell dimensions.
|
// the natural cell dimensions.
|
||||||
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
||||||
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
||||||
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
|
const offsetCanvas = createCanvas(window.document, canvasW, canvasH);
|
||||||
const offsetCtx = offsetCanvas.getContext('2d');
|
const offsetCtx = offsetCanvas.getContext('2d');
|
||||||
if (!offsetCtx) {
|
if (!offsetCtx) {
|
||||||
throw new Error('Failed to create offset canvas context');
|
throw new Error('Failed to create offset canvas context');
|
||||||
@@ -700,7 +701,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
return new Promise<ImageBitmap>((resolve, reject) => {
|
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||||
img.addEventListener('load', () => {
|
img.addEventListener('load', () => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
|
const canvas = createCanvas(window.document, img.width, img.height);
|
||||||
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
||||||
createImageBitmap(canvas).then(resolve).catch(reject);
|
createImageBitmap(canvas).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { IDisposable } from '@xterm/xterm';
|
|||||||
import { ImageStorage } from '../ImageStorage';
|
import { ImageStorage } from '../ImageStorage';
|
||||||
import { ImageLayer, IAddImageOpts } from '../Types';
|
import { ImageLayer, IAddImageOpts } from '../Types';
|
||||||
import { IKittyImageData } from './KittyGraphicsTypes';
|
import { IKittyImageData } from './KittyGraphicsTypes';
|
||||||
|
import { Drawable } from '../Primitives';
|
||||||
|
import { UNSUPPORTED_TYPE } from '../Metrics';
|
||||||
|
|
||||||
// Kitty-specific image storage controller.
|
// Kitty-specific image storage controller.
|
||||||
//
|
//
|
||||||
@@ -102,7 +104,8 @@ export class KittyImageStorage implements IDisposable {
|
|||||||
this._addImageOpts.scrolling = scrolling;
|
this._addImageOpts.scrolling = scrolling;
|
||||||
this._addImageOpts.layer = layer;
|
this._addImageOpts.layer = layer;
|
||||||
this._addImageOpts.zIndex = zIndex;
|
this._addImageOpts.zIndex = zIndex;
|
||||||
const storageId = this._storage.addImage(image, this._addImageOpts);
|
// FIXME: store image data in blob, fill metrics correctly
|
||||||
|
const storageId = this._storage.addImage(new Drawable(image), undefined, UNSUPPORTED_TYPE, this._addImageOpts);
|
||||||
this._kittyIdToStorageId.set(kittyId, storageId);
|
this._kittyIdToStorageId.set(kittyId, storageId);
|
||||||
this._storageIdToKittyId.set(storageId, kittyId);
|
this._storageIdToKittyId.set(storageId, kittyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,10 +294,10 @@ test.describe('ImageAddon', () => {
|
|||||||
test('size params is optional/informative', async () => {
|
test('size params is optional/informative', async () => {
|
||||||
// too small size param
|
// too small size param
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
// no size aparm at all
|
// no size aparm at all
|
||||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
deepStrictEqual(await getImageSize(2), [640, 80]);
|
||||||
});
|
});
|
||||||
test.skip('FilePart support - bytewise', async () => {
|
test.skip('FilePart support - bytewise', async () => {
|
||||||
// opener
|
// opener
|
||||||
@@ -308,7 +308,7 @@ test.describe('ImageAddon', () => {
|
|||||||
}
|
}
|
||||||
// finalizer
|
// finalizer
|
||||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
});
|
});
|
||||||
test('FilePart support - chunks', async () => {
|
test('FilePart support - chunks', async () => {
|
||||||
// opener
|
// opener
|
||||||
@@ -319,7 +319,7 @@ test.describe('ImageAddon', () => {
|
|||||||
}
|
}
|
||||||
// finalizer
|
// finalizer
|
||||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
});
|
});
|
||||||
test('FilePart support - edgecases', async () => {
|
test('FilePart support - edgecases', async () => {
|
||||||
// multiple opener do not harm
|
// multiple opener do not harm
|
||||||
@@ -347,39 +347,39 @@ test.describe('ImageAddon', () => {
|
|||||||
// finalizer skipped
|
// finalizer skipped
|
||||||
// write spinfox.png
|
// write spinfox.png
|
||||||
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('IIP support - testimages', () => {
|
test.describe('IIP support - testimages', () => {
|
||||||
test('palette.png', async () => {
|
test('palette.png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[0][0]);
|
await ctx.proxy.write(TESTDATA_IIP[0][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[0][1]);
|
||||||
});
|
});
|
||||||
test('spinfox.png', async () => {
|
test('spinfox.png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]);
|
||||||
});
|
});
|
||||||
test('w3c gif', async () => {
|
test('w3c gif', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[2][0]);
|
await ctx.proxy.write(TESTDATA_IIP[2][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[2][1]);
|
||||||
});
|
});
|
||||||
test('w3c jpeg', async () => {
|
test('w3c jpeg', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[3][0]);
|
await ctx.proxy.write(TESTDATA_IIP[3][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[3][1]);
|
||||||
});
|
});
|
||||||
test('w3c png', async () => {
|
test('w3c png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[4][0]);
|
await ctx.proxy.write(TESTDATA_IIP[4][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[4][1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('IIP - QOI support', () => {
|
test.describe('IIP - QOI support', () => {
|
||||||
test('palette should yield same bytes from PNG and QOI', async () => {
|
test('palette should yield same bytes from PNG and QOI', async () => {
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
deepStrictEqual(await getImageSize(2), [640, 80]);
|
||||||
const pngScrape = await getImageAtBufferCell(0, 0);
|
const pngScrape = await getImageAtBufferCell(0, 0);
|
||||||
const qoiScrape = await getImageAtBufferCell(0, 11);
|
const qoiScrape = await getImageAtBufferCell(0, 11);
|
||||||
deepStrictEqual(qoiScrape, pngScrape);
|
deepStrictEqual(qoiScrape, pngScrape);
|
||||||
@@ -406,20 +406,20 @@ test.describe('ImageAddon', () => {
|
|||||||
const header = 'width=20;height=5;preserveAspectRatio=0';
|
const header = 'width=20;height=5;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
deepStrictEqual(await getOrigSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
|
deepStrictEqual(await getImageSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
|
||||||
});
|
});
|
||||||
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
|
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
|
||||||
// pixel based resize
|
// pixel based resize
|
||||||
const header = 'width=320px;height=160px;preserveAspectRatio=0';
|
const header = 'width=320px;height=160px;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [320, 160]);
|
deepStrictEqual(await getImageSize(1), [320, 160]);
|
||||||
});
|
});
|
||||||
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
|
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
|
||||||
// % of viewport resize
|
// % of viewport resize
|
||||||
const header = 'width=50%;height=30%;preserveAspectRatio=0';
|
const header = 'width=50%;height=30%;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
deepStrictEqual(await getOrigSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
|
deepStrictEqual(await getImageSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
|
||||||
});
|
});
|
||||||
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
|
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
|
||||||
// width provided in percent
|
// width provided in percent
|
||||||
@@ -427,11 +427,11 @@ test.describe('ImageAddon', () => {
|
|||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
const width = Math.floor(dim.width * 0.5);
|
const width = Math.floor(dim.width * 0.5);
|
||||||
deepStrictEqual(await getOrigSize(1), [width, Math.floor(width * 80 / 640)]);
|
deepStrictEqual(await getImageSize(1), [width, Math.floor(width * 80 / 640)]);
|
||||||
// height provided in pixel
|
// height provided in pixel
|
||||||
const header2 = 'height=200px';
|
const header2 = 'height=200px';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [Math.floor(200 * 640 / 80), 200]);
|
deepStrictEqual(await getImageSize(2), [Math.floor(200 * 640 / 80), 200]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -452,10 +452,10 @@ test.describe('ImageAddon', () => {
|
|||||||
async function getDimensions(): Promise<IDimensions> {
|
async function getDimensions(): Promise<IDimensions> {
|
||||||
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
||||||
return {
|
return {
|
||||||
cellWidth: Math.round(dimensions.css.cell.width),
|
cellWidth: Math.round(dimensions.device.cell.width),
|
||||||
cellHeight: Math.round(dimensions.css.cell.height),
|
cellHeight: Math.round(dimensions.device.cell.height),
|
||||||
width: Math.round(dimensions.css.canvas.width),
|
width: Math.round(dimensions.device.canvas.width),
|
||||||
height: Math.round(dimensions.css.canvas.height)
|
height: Math.round(dimensions.device.canvas.height)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,11 +471,11 @@ async function getScrollbackPlusRows(): Promise<number> {
|
|||||||
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
|
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
async function getImageSize(id: number): Promise<[number, number]> {
|
||||||
return ctx.page.evaluate<any>(`[
|
return ctx.page.evaluate<any>(`[
|
||||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
window.imageAddon._storage._images.get(${id}).src.width * window.imageAddon._storage._images.get(${id}).prescaleX,
|
||||||
window.imageAddon._storage._images.get(${id}).orig.height
|
window.imageAddon._storage._images.get(${id}).src.height * window.imageAddon._storage._images.get(${id}).prescaleY
|
||||||
]`);
|
]`).then(r => r.map(Math.floor));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
||||||
@@ -483,12 +483,12 @@ async function getImageAtBufferCell(x: number, y: number): Promise<string | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||||
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
return ctx.page.evaluate(`!!window.imageAddon.getImageAtBufferCell(${x}, ${y})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
|
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
|
||||||
await ctx.proxy.write('\x1b[H' + imageSeq);
|
await ctx.proxy.write('\x1b[H' + imageSeq);
|
||||||
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(5, 1)', true);
|
||||||
ok(await hasTileAtBufferCell(0, 1));
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
await ctx.proxy.write('\x1b[2;6H#######');
|
await ctx.proxy.write('\x1b[2;6H#######');
|
||||||
for (let x = 5; x < 12; x++) {
|
for (let x = 5; x < 12; x++) {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(seq);
|
await ctx.proxy.write(seq);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [1, 1]);
|
deepStrictEqual(await getImageSize(1), [1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores 3x1 RGB PNG with a=T', async () => {
|
test('stores 3x1 RGB PNG with a=T', async () => {
|
||||||
@@ -131,7 +131,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(seq);
|
await ctx.proxy.write(seq);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transmit only (a=t) does not display but stores in handler', async () => {
|
test('transmit only (a=t) does not display but stores in handler', async () => {
|
||||||
@@ -1107,7 +1107,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [20, 50]);
|
deepStrictEqual(await getImageSize(1), [20, 50]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1283,7 +1283,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
|
|
||||||
const cursor = await getCursor();
|
const cursor = await getCursor();
|
||||||
const cellDims: number[] = await ctx.page.evaluate(() => {
|
const cellDims: number[] = await ctx.page.evaluate(() => {
|
||||||
const d = (window as any).term._core._renderService.dimensions.css.cell;
|
const d = (window as any).term._core._renderService.dimensions.device.cell;
|
||||||
return [d.width, d.height];
|
return [d.width, d.height];
|
||||||
});
|
});
|
||||||
const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]);
|
const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]);
|
||||||
@@ -1301,7 +1301,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
|
|
||||||
const cursor = await getCursor();
|
const cursor = await getCursor();
|
||||||
const cellDims: number[] = await ctx.page.evaluate(() => {
|
const cellDims: number[] = await ctx.page.evaluate(() => {
|
||||||
const d = (window as any).term._core._renderService.dimensions.css.cell;
|
const d = (window as any).term._core._renderService.dimensions.device.cell;
|
||||||
return [d.width, d.height];
|
return [d.width, d.height];
|
||||||
});
|
});
|
||||||
const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]);
|
const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]);
|
||||||
@@ -1535,7 +1535,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transmit only (a=t) stores 200x100 image without display', async () => {
|
test('transmit only (a=t) stores 200x100 image without display', async () => {
|
||||||
@@ -1564,7 +1564,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles 3-chunk transmission', async () => {
|
test('handles 3-chunk transmission', async () => {
|
||||||
@@ -1580,7 +1580,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('verifies chunked data assembles correctly', async () => {
|
test('verifies chunked data assembles correctly', async () => {
|
||||||
@@ -1713,7 +1713,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [20, 50]);
|
deepStrictEqual(await getImageSize(1), [20, 50]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]);
|
||||||
});
|
});
|
||||||
@@ -1747,7 +1747,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
|
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
|
||||||
});
|
});
|
||||||
@@ -1756,7 +1756,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
|
||||||
});
|
});
|
||||||
@@ -1772,7 +1772,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1893,28 +1893,28 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (2x2)', async () => {
|
test('stores image with correct original dimensions (2x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [2, 2]);
|
deepStrictEqual(await getImageSize(1), [2, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (5x1)', async () => {
|
test('stores image with correct original dimensions (5x1)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [5, 1]);
|
deepStrictEqual(await getImageSize(1), [5, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (4x2)', async () => {
|
test('stores image with correct original dimensions (4x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [4, 2]);
|
deepStrictEqual(await getImageSize(1), [4, 2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2034,21 +2034,21 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (2x2)', async () => {
|
test('stores image with correct original dimensions (2x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [2, 2]);
|
deepStrictEqual(await getImageSize(1), [2, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (5x1)', async () => {
|
test('stores image with correct original dimensions (5x1)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [5, 1]);
|
deepStrictEqual(await getImageSize(1), [5, 1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2217,7 +2217,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
test.describe('text overwrite removes tiles', () => {
|
test.describe('text overwrite removes tiles', () => {
|
||||||
test('a=T placement', async () => {
|
test('a=T placement', async () => {
|
||||||
await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(5, 1)', true);
|
||||||
ok(await hasTileAtBufferCell(0, 1));
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
await ctx.proxy.write('\x1b[2;6H#######');
|
await ctx.proxy.write('\x1b[2;6H#######');
|
||||||
for (let x = 5; x < 12; x++) {
|
for (let x = 5; x < 12; x++) {
|
||||||
@@ -2236,10 +2236,10 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
async function getDimensions(): Promise<IDimensions> {
|
async function getDimensions(): Promise<IDimensions> {
|
||||||
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
||||||
return {
|
return {
|
||||||
cellWidth: Math.round(dimensions.css.cell.width),
|
cellWidth: Math.round(dimensions.device.cell.width),
|
||||||
cellHeight: Math.round(dimensions.css.cell.height),
|
cellHeight: Math.round(dimensions.device.cell.height),
|
||||||
width: Math.round(dimensions.css.canvas.width),
|
width: Math.round(dimensions.device.canvas.width),
|
||||||
height: Math.round(dimensions.css.canvas.height)
|
height: Math.round(dimensions.device.canvas.height)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2251,10 +2251,10 @@ async function getImageStorageLength(): Promise<number> {
|
|||||||
return ctx.page.evaluate('window.imageAddon._storage._images.size');
|
return ctx.page.evaluate('window.imageAddon._storage._images.size');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
async function getImageSize(id: number): Promise<[number, number]> {
|
||||||
return ctx.page.evaluate<any>(`[
|
return ctx.page.evaluate<any>(`[
|
||||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
window.imageAddon._storage._images.get(${id}).src.width,
|
||||||
window.imageAddon._storage._images.get(${id}).orig.height
|
window.imageAddon._storage._images.get(${id}).src.height
|
||||||
]`);
|
]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2279,5 +2279,5 @@ async function getPixels(col: number, row: number, x: number, y: number, w: numb
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||||
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
return ctx.page.evaluate(`!!window.imageAddon.getImageAtBufferCell(${x}, ${y})`);
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -125,10 +125,5 @@ declare module '@xterm/addon-image' {
|
|||||||
* Get original image canvas at buffer position.
|
* Get original image canvas at buffer position.
|
||||||
*/
|
*/
|
||||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract single tile canvas at buffer position.
|
|
||||||
*/
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,11 +203,8 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
|||||||
const pos = (this._terminal as any)._core._mouseCoordsService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
const pos = (this._terminal as any)._core._mouseCoordsService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
||||||
const x = pos[0] - 1;
|
const x = pos[0] - 1;
|
||||||
const y = pos[1] - 1;
|
const y = pos[1] - 1;
|
||||||
const canvas = ev.shiftKey
|
// ctrl+click: get original image
|
||||||
// ctrl+shift+click: get single tile
|
const canvas = imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
|
||||||
? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
|
|
||||||
// ctrl+click: get original image
|
|
||||||
: imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
|
|
||||||
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
|
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1137,13 +1137,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
|
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
|
||||||
const canvasWidth = this._renderService.dimensions.css.canvas.width.toFixed(0);
|
const canvasWidth = this._renderService.dimensions.device.canvas.width.toFixed(0);
|
||||||
const canvasHeight = this._renderService.dimensions.css.canvas.height.toFixed(0);
|
const canvasHeight = this._renderService.dimensions.device.canvas.height.toFixed(0);
|
||||||
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
||||||
break;
|
break;
|
||||||
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
|
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
|
||||||
const cellWidth = this._renderService.dimensions.css.cell.width.toFixed(0);
|
const cellWidth = this._renderService.dimensions.device.cell.width.toFixed(0);
|
||||||
const cellHeight = this._renderService.dimensions.css.cell.height.toFixed(0);
|
const cellHeight = this._renderService.dimensions.device.cell.height.toFixed(0);
|
||||||
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user