Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

image addon fixes #4631

Merged
merged 6 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"sourceType": "module"
},
"ignorePatterns": [
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js"
Expand Down
32 changes: 3 additions & 29 deletions addons/xterm-addon-image/README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,17 @@
## xterm-addon-image

Image output in xterm.js.
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).


![](fixture/example.png)


### Important note

Version 0.4.x will be the last version from this single repo.
Future versions will reside as addon in the xterm.js main repo.


### Install from npm

```bash
npm install --save xterm-addon-image
```

### Release Compatibility

- 0.4.2 - compatible to xterm.js 5.2.0
- 0.4.1 - compatible to xterm.js 5.2.0
- 0.4.0 - compatible to xterm.js 5.1.0
- 0.3.1 - compatible to xterm.js 5.1.0
- 0.3.0 - compatible to xterm.js 5.0.0
- 0.2.0 - compatible to xterm.js 5.0.0
- 0.1.x - compatible to xterm.js 4.16.0 - 4.19.0


### Clone & Build

The addon integrates tightly with the xterm.js base repo, esp. for tests and the demo.
To properly set up all needed resources see `bootstrap.sh` or run it directly with

```bash
curl -s https://raw.githubusercontent.com/jerch/xterm-addon-image/master/bootstrap.sh | XTERMJS=5.2.0 IMAGEADDON=master bash
```

The addon sources and npm package definition reside under `addons/xterm-addon-image`.


### Usage

Expand Down Expand Up @@ -243,6 +215,8 @@ _How can I adjust the memory usage?_

### Changelog

- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
- 0.4.3 defer canvas creation
- 0.4.2 fix image canvas resize
- 0.4.1 compat release for xterm.js 5.2.0
- 0.4.0 IIP support
Expand Down
9 changes: 4 additions & 5 deletions addons/xterm-addon-image/src/IIPHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,13 @@ export class IIPHandler implements IOscHandler, IResetHandler {
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
this._dec.release();

const win = this._coreTerminal._core._coreBrowserService.window;
if (!win.createImageBitmap) {
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise<boolean>(r => {
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
const canvas = ImageRenderer.createCanvas(win, w, h);
const canvas = ImageRenderer.createCanvas(window.document, w, h);
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
this._storage.addImage(canvas);
r(true);
Expand All @@ -126,9 +125,9 @@ export class IIPHandler implements IOscHandler, IResetHandler {
setTimeout(() => r(true), 1000);
});
}
return win.createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
.then(bm => {
this._storage.addImage(bm as unknown as HTMLCanvasElement);
this._storage.addImage(bm);
return true;
});
}
Expand Down
2 changes: 1 addition & 1 deletion addons/xterm-addon-image/src/ImageAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class ImageAddon implements ITerminalAddon {
this._terminal = terminal;

// internal data structures
this._renderer = new ImageRenderer(terminal, this._opts.showPlaceholder);
this._renderer = new ImageRenderer(terminal);
this._storage = new ImageStorage(terminal, this._renderer, this._opts);

// enable size reports
Expand Down
72 changes: 49 additions & 23 deletions addons/xterm-addon-image/src/ImageRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ export class ImageRenderer implements IDisposable {
private _oldSetRenderer: ((renderer: any) => void) | undefined;

// drawing primitive - canvas
public static createCanvas(base: Window, width: number, height: number): HTMLCanvasElement {
const canvas = base.document.createElement('canvas');
public static createCanvas(localDocument: Document | undefined, width: number, height: 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;
return canvas;
Expand Down Expand Up @@ -58,7 +67,7 @@ export class ImageRenderer implements IDisposable {
}


constructor(private _terminal: ITerminalExt, private _showPlaceholder: boolean) {
constructor(private _terminal: ITerminalExt) {
this._oldOpen = this._terminal._core.open;
this._terminal._core.open = (parent: HTMLElement): void => {
this._oldOpen?.call(this._terminal._core, parent);
Expand All @@ -79,7 +88,7 @@ export class ImageRenderer implements IDisposable {

public dispose(): void {
this._optionsRefresh?.dispose();
this._removeLayerFromDom();
this.removeLayerFromDom();
if (this._terminal._core && this._oldOpen) {
this._terminal._core.open = this._oldOpen;
this._oldOpen = undefined;
Expand Down Expand Up @@ -204,7 +213,7 @@ export class ImageRenderer implements IDisposable {
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._terminal._core._coreBrowserService.window, finalWidth, finalHeight);
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
Expand All @@ -220,17 +229,20 @@ export class ImageRenderer implements IDisposable {
* Draw a line with placeholder on the image layer canvas.
*/
public drawPlaceholder(col: number, row: number, count: number = 1): void {
if ((this._placeholderBitmap || this._placeholder) && this._ctx) {
if (this._ctx) {
const { width, height } = this.cellSize;

// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}

if (height >= this._placeholder!.height) {
if (!this._placeholder) {
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
} else if (height >= this._placeholder!.height) {
this._createPlaceHolder(height + 1);
}
if (!this._placeholder) return;
this._ctx.drawImage(
this._placeholderBitmap || this._placeholder!,
col * width,
Expand Down Expand Up @@ -274,7 +286,7 @@ export class ImageRenderer implements IDisposable {
return;
}
const canvas = ImageRenderer.createCanvas(
this._terminal._core._coreBrowserService.window,
this.document,
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
);
Expand All @@ -294,25 +306,35 @@ export class ImageRenderer implements IDisposable {
this._renderService = this._terminal._core._renderService;
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
this._renderService.setRenderer = (renderer: any) => {
this._removeLayerFromDom();
this.removeLayerFromDom();
this._oldSetRenderer?.call(this._renderService, renderer);
this._insertLayerToDom();
};
this._insertLayerToDom();
if (this._showPlaceholder) {
this._createPlaceHolder();
}
}

private _insertLayerToDom(): void {
this.canvas = ImageRenderer.createCanvas(this._terminal._core._coreBrowserService.window, this.dimensions?.css.canvas.width || 0, this.dimensions?.css.canvas.height || 0);
this.canvas.classList.add('xterm-image-layer');
this._terminal._core.screenElement?.appendChild(this.canvas);
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
public insertLayerToDom(): void {
// make sure that the terminal is attached to a document and to DOM
if (this.document && this._terminal._core.screenElement) {
if (!this.canvas) {
this.canvas = ImageRenderer.createCanvas(
this.document, this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0
);
this.canvas.classList.add('xterm-image-layer');
this._terminal._core.screenElement.appendChild(this.canvas);
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
this.clearAll();
}
} else {
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
}
}

private _removeLayerFromDom(): void {
this.canvas?.parentNode?.removeChild(this.canvas);
public removeLayerFromDom(): void {
if (this.canvas) {
this._ctx = undefined;
this.canvas.remove();
this.canvas = undefined;
}
}

private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
Expand All @@ -321,7 +343,7 @@ export class ImageRenderer implements IDisposable {

// create blueprint to fill placeholder with
const bWidth = 32; // must be 2^n
const blueprint = ImageRenderer.createCanvas(this._terminal._core._coreBrowserService.window, bWidth, height);
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
const ctx = blueprint.getContext('2d', { alpha: false });
if (!ctx) return;
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
Expand All @@ -340,7 +362,7 @@ export class ImageRenderer implements IDisposable {

// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
this._placeholder = ImageRenderer.createCanvas(this._terminal._core._coreBrowserService.window, width, height);
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
this._placeholder = undefined;
Expand All @@ -351,4 +373,8 @@ export class ImageRenderer implements IDisposable {
}
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
}

public get document(): Document | undefined {
return this._terminal._core._coreBrowserService?.window.document;
}
}
27 changes: 16 additions & 11 deletions addons/xterm-addon-image/src/ImageStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ export class ImageStorage implements IDisposable {
}

public setLimit(value: number): void {
if (value < 1 || value > 1000) {
throw RangeError('invalid storageLimit, should be at least 1 MB and not exceed 1G');
if (value < 0.5 || value > 1000) {
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
}
this._pixelLimit = (value / 4 * 1000000) >>> 0;
this._evictOldest(0);
Expand All @@ -177,8 +177,7 @@ export class ImageStorage implements IDisposable {
const spec = this._images.get(id);
this._images.delete(id);
// FIXME: really ugly workaround to get bitmaps deallocated :(
const win = this._terminal._core._coreBrowserService.window;
if (spec && win.ImageBitmap && spec.orig instanceof ImageBitmap) {
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
spec.orig.close();
}
}
Expand Down Expand Up @@ -224,7 +223,7 @@ export class ImageStorage implements IDisposable {
/**
* Method to add an image to the storage.
*/
public addImage(img: HTMLCanvasElement): void {
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);

Expand Down Expand Up @@ -327,9 +326,13 @@ export class ImageStorage implements IDisposable {
*/
// TODO: Should we move this to the ImageRenderer?
public render(range: { start: number, end: number }): void {
// exit early if we dont have a canvas
if (!this._renderer.canvas) {
return;
// setup image canvas in case we have none yet, but have images in store
if (!this._renderer.canvas && this._images.size) {
this._renderer.insertLayerToDom();
// safety measure - in case we cannot spawn a canvas at all, just exit
if (!this._renderer.canvas) {
return;
}
}
// rescale if needed
this._renderer.rescaleCanvas();
Expand All @@ -340,6 +343,9 @@ export class ImageStorage implements IDisposable {
this._fullyCleared = true;
this._needsFullClear = false;
}
if (this._renderer.canvas) {
this._renderer.removeLayerFromDom();
}
return;
}

Expand Down Expand Up @@ -472,9 +478,8 @@ export class ImageStorage implements IDisposable {
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
const win = this._terminal._core._coreBrowserService.window;
if (win.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(win, orig.width, orig.height);
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
return canvas;
}
Expand Down
11 changes: 8 additions & 3 deletions addons/xterm-addon-image/src/SixelHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
if (this._dec) {
const fillColor = params.params[1] === 1 ? 0 : extractActiveBg(
this._coreTerminal._core._inputHandler._curAttrData,
this._coreTerminal._core._themeService.colors);
this._coreTerminal._core._themeService?.colors);
this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
}
}
Expand Down Expand Up @@ -98,7 +98,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
return true;
}

const canvas = ImageRenderer.createCanvas(this._coreTerminal._core._coreBrowserService.window, width, height);
const canvas = ImageRenderer.createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
Expand All @@ -115,8 +115,13 @@ export class SixelHandler implements IDcsHandler, IResetHandler {

// get currently active background color from terminal
// also respect INVERSE setting
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet): RGBA8888 {
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet | undefined): RGBA8888 {
let bg = 0;
if (!colors) {
// FIXME: theme service is prolly not available yet,
// happens if .open() was not called yet (bug in core?)
return bg;
}
if (attr.isInverse()) {
if (attr.isFgDefault()) {
bg = convertLe(colors.foreground.rgba);
Expand Down
4 changes: 2 additions & 2 deletions addons/xterm-addon-image/src/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ interface IInputHandlerExt extends IInputHandler {
}

export interface ICoreTerminalExt extends ITerminal {
_themeService: IThemeService;
_themeService: IThemeService | undefined;
_inputHandler: IInputHandlerExt;
_renderService: IRenderService;
_coreBrowserService: ICoreBrowserService;
_coreBrowserService: ICoreBrowserService | undefined;
}

export interface ITerminalExt extends Terminal {
Expand Down