From b7ef9fa25e98c3ba762fb5b89e046ef87b4ba661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 30 Aug 2026 22:53:15 +0200 Subject: [PATCH] cleanup primitives and move to own file --- addons/addon-image/src/IIPHandler.ts | 3 +- addons/addon-image/src/ImageRenderer.ts | 58 ++++--------------- addons/addon-image/src/ImageStorage.ts | 3 +- addons/addon-image/src/Primitives.ts | 21 +++++++ addons/addon-image/src/SixelHandler.ts | 4 +- .../src/kitty/KittyGraphicsHandler.ts | 5 +- 6 files changed, 40 insertions(+), 54 deletions(-) create mode 100644 addons/addon-image/src/Primitives.ts diff --git a/addons/addon-image/src/IIPHandler.ts b/addons/addon-image/src/IIPHandler.ts index 559b90741..d26a53faa 100644 --- a/addons/addon-image/src/IIPHandler.ts +++ b/addons/addon-image/src/IIPHandler.ts @@ -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; diff --git a/addons/addon-image/src/ImageRenderer.ts b/addons/addon-image/src/ImageRenderer.ts index 5854efaec..84d83e5e5 100644 --- a/addons/addon-image/src/ImageRenderer.ts +++ b/addons/addon-image/src/ImageRenderer.ts @@ -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 { - 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 { diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 5dfca3f69..17006f26d 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -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; } diff --git a/addons/addon-image/src/Primitives.ts b/addons/addon-image/src/Primitives.ts new file mode 100644 index 000000000..86a3c5121 --- /dev/null +++ b/addons/addon-image/src/Primitives.ts @@ -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; +} + diff --git a/addons/addon-image/src/SixelHandler.ts b/addons/addon-image/src/SixelHandler.ts index 1af2d85bc..92b3f337c 100644 --- a/addons/addon-image/src/SixelHandler.ts +++ b/addons/addon-image/src/SixelHandler.ts @@ -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, width, height), 0, 0); if (this._dec.memoryUsage > MEM_PERMA_LIMIT) { this._dec.release(); diff --git a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts index de889dfff..2458c8094 100644 --- a/addons/addon-image/src/kitty/KittyGraphicsHandler.ts +++ b/addons/addon-image/src/kitty/KittyGraphicsHandler.ts @@ -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((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); });