cleanup primitives and move to own file

This commit is contained in:
Jörg Breitbart
2026-08-30 22:53:15 +02:00
parent c58ea3637f
commit b7ef9fa25e
6 changed files with 40 additions and 54 deletions
+2 -1
View File
@@ -10,6 +10,7 @@ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
import { createCanvas } from './Primitives';
// Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst {
@@ -189,7 +190,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
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);
const canvas = createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
canvas.getContext('2d')?.putImageData(blob, 0, 0);
this._storage.addImage(canvas);
return true;
+10 -48
View File
@@ -7,6 +7,7 @@ import { toRGBA8888 } from 'sixel/lib/Colors';
import { IDisposable } from '@xterm/xterm';
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { createCanvas } from './Primitives';
const enum Constants {
PLACEHOLDER_LENGTH = 4096,
@@ -30,46 +31,6 @@ export class ImageRenderer extends Disposable implements IDisposable {
private _renderService: IRenderService | undefined;
private _oldSetRenderer: ((renderer: any) => void) | undefined;
// drawing primitive - 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;
}
// 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) {
super();
this._oldOpen = this._terminal._core.open;
@@ -225,7 +186,7 @@ export class ImageRenderer extends Disposable 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.document, finalWidth, finalHeight);
const canvas = createCanvas(this.document, finalWidth, finalHeight);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
@@ -299,7 +260,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
spec.actualCellSize.height = originalHeight;
return;
}
const canvas = ImageRenderer.createCanvas(
const canvas = createCanvas(
this.document,
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
@@ -336,8 +297,9 @@ export class ImageRenderer extends Disposable implements IDisposable {
if (this._layers.has(layer)) {
return;
}
const canvas = ImageRenderer.createCanvas(
this.document, this.dimensions?.css.canvas.width || 0,
const canvas = createCanvas(
this.document,
this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0
);
canvas.classList.add(`xterm-image-layer-${layer}`);
@@ -387,10 +349,10 @@ export class ImageRenderer extends Disposable implements IDisposable {
// create blueprint to fill placeholder with
const bWidth = 32; // must be 2^n
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
const blueprint = createCanvas(this.document, bWidth, height);
const ctx = blueprint.getContext('2d', { alpha: false });
if (!ctx) return;
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
const imgData = new ImageData(bWidth, height);
const d32 = new Uint32Array(imgData.data.buffer);
const black = toRGBA8888(0, 0, 0);
const white = toRGBA8888(255, 255, 255);
@@ -406,7 +368,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
// 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);
this._placeholder = createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
this._placeholder = undefined;
@@ -415,7 +377,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
for (let i = 0; i < width; i += bWidth) {
ctx2.drawImage(blueprint, i, 0);
}
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
}
public get document(): Document | undefined {
+2 -1
View File
@@ -10,6 +10,7 @@ import type {
} from './Types';
import type { IBufferLine } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { createCanvas } from './Primitives';
// fallback default cell size
export const CELL_SIZE_DEFAULT: ICellSize = {
@@ -472,7 +473,7 @@ export class ImageStorage implements IDisposable {
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
const canvas = createCanvas(window.document, orig.width, orig.height);
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
return canvas;
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function 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;
}
+2 -2
View File
@@ -7,9 +7,9 @@ import { SixelImageStorage } from './SixelImageStorage';
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 { RGBA8888 } from 'sixel/lib/Types';
import { ImageRenderer } from './ImageRenderer';
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
import { createCanvas } from './Primitives';
// always free decoder ressources after decoding if it exceeds this limit
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
@@ -98,7 +98,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
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);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
@@ -19,6 +19,7 @@ import {
KittyPixelConstants,
parseKittyCommand
} from './KittyGraphicsTypes';
import { createCanvas } from '../Primitives';
const enum Constants {
// Memory limit for base64 decoder (4MB, same as IIPHandler)
@@ -636,7 +637,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// the natural cell dimensions.
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 offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
const offsetCanvas = createCanvas(window.document, canvasW, canvasH);
const offsetCtx = offsetCanvas.getContext('2d');
if (!offsetCtx) {
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) => {
img.addEventListener('load', () => {
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);
createImageBitmap(canvas).then(resolve).catch(reject);
});