From 42894ff9a148f4528d72fc30b650403f8c29e48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 28 Aug 2026 22:10:31 +0200 Subject: [PATCH] pass metrics for correct rescaling, minor fixes --- addons/addon-image/src/AnimationManager.ts | 30 ++++++++++++---------- addons/addon-image/src/IIPHandler.ts | 2 +- addons/addon-image/src/ImageAddon.ts | 7 ++--- addons/addon-image/src/Types.ts | 3 ++- addons/addon-image/test/ImageAddon.test.ts | 13 ++++++---- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/addons/addon-image/src/AnimationManager.ts b/addons/addon-image/src/AnimationManager.ts index 25a1cca62..5e87dd3e4 100644 --- a/addons/addon-image/src/AnimationManager.ts +++ b/addons/addon-image/src/AnimationManager.ts @@ -7,6 +7,7 @@ import { IDisposable } from '@xterm/xterm'; import type { ImageRenderer } from './ImageRenderer'; import { EMPTY_ATTRS, type ImageStorage } from './ImageStorage'; import { BgFlags, IBufferLineExt, IImageSpec, ITerminalExt, IExtendedAttrsImage, IResetHandler } from './Types'; +import { IMetrics } from 'IIPMetrics'; /** @@ -30,8 +31,8 @@ interface IAnimation { id: number; /** image bytes */ data: Blob; - /** mime type */ - mime: string; + /** metrics of the image */ + metrics: IMetrics; /** width */ w: number; /** height */ @@ -258,8 +259,8 @@ export class AnimationManager implements IDisposable, IResetHandler { * @param w Output width in pixel. * @param h Output height in pixel. */ - public async registerAnimation(id: number, data: Blob, mime: string, w: number, h: number): Promise { - let decoder = await BlobDecoder(data, mime); + public async registerAnimation(id: number, data: Blob, metrics: IMetrics, w: number, h: number): Promise { + let decoder = await BlobDecoder(data, metrics.mime); const track = decoder.tracks.selectedTrack; if (!track || track.frameCount < 2) { decoder.close(); @@ -282,7 +283,7 @@ export class AnimationManager implements IDisposable, IResetHandler { frame.close(); // skip wrong dimensions and zero duration // FIXME: needs orig image dimensions in IAnimation - if (width !== w || height !== h || !duration) { + if (width !== metrics.width || height !== metrics.height || !duration) { if (durations.length) { durations[durations.length - 1] += duration; elapsed += duration; @@ -317,11 +318,12 @@ export class AnimationManager implements IDisposable, IResetHandler { if (isFirefox) { decoder.close(); - decoder = await BlobDecoder(data, mime); + decoder = await BlobDecoder(data, metrics.mime); } const anim: IAnimation = { - id, data, mime, w, h, decoder, + id, data, w, h, decoder, + metrics, numFrames: frames.length, decoderPromise: undefined, cache: new BitmapBuffer(), @@ -340,7 +342,7 @@ export class AnimationManager implements IDisposable, IResetHandler { this.viewportUpdated(); // early schedule to give decoder time to fill the cache before RAF this._schedule(anim); - this._animationFrame ??= this._requestAF(ts => this._loop(ts)); + this._animationFrame ??= this._requestAF(this._loop); } @@ -363,7 +365,7 @@ export class AnimationManager implements IDisposable, IResetHandler { * Scratches the viewport buffer to collect draws for animation RAF * and to pause/resume invisible/visible animations. */ - public viewportUpdated(range?: { start: number, end: number }): any { + public viewportUpdated(_range?: { start: number, end: number }): void { const rows = this._terminal.rows; const cols = this._terminal.cols; const buffer = this._terminal._core.buffer; @@ -452,7 +454,7 @@ export class AnimationManager implements IDisposable, IResetHandler { anim.frameIdx = frameIdx; anim.current = anim.frames[frameIdx]; if (!anim.decoder && !anim.decoderPromise) { - anim.decoderPromise = BlobDecoder(anim.data, anim.mime); + anim.decoderPromise = BlobDecoder(anim.data, anim.metrics.mime); } this._schedule(anim); } else if (!isVisible && anim.inViewport) { @@ -462,7 +464,7 @@ export class AnimationManager implements IDisposable, IResetHandler { } } if (visibles.size && this._animationFrame === undefined) { - this._animationFrame = this._requestAF(ts => this._loop(ts)); + this._animationFrame = this._requestAF(this._loop); } } @@ -528,7 +530,7 @@ export class AnimationManager implements IDisposable, IResetHandler { this._renderer.draw(d.imgSpec, d.tileId, d.col, d.row, d.count); } } - this._animationFrame = anyVisible ? this._requestAF(ts => this._loop(ts)) : undefined; + this._animationFrame = anyVisible ? this._requestAF(this._loop) : undefined; }; @@ -571,7 +573,7 @@ export class AnimationManager implements IDisposable, IResetHandler { if (!a.decoder) { const decP = a.decoderPromise; a.decoderPromise = undefined; - a.decoder = await (decP ?? BlobDecoder(a.data, a.mime)); + a.decoder = await (decP ?? BlobDecoder(a.data, a.metrics.mime)); } const n1 = a.frames[(a.frameIdx + 1) % a.numFrames]; const n2 = a.frames[(a.frameIdx + 2) % a.numFrames]; @@ -582,7 +584,7 @@ export class AnimationManager implements IDisposable, IResetHandler { if (n <= a.highest) { a.highest = -1; a.decoder?.close(); - a.decoder = await BlobDecoder(a.data, a.mime); + a.decoder = await BlobDecoder(a.data, a.metrics.mime); } a.highest = Math.max(n, a.highest); } diff --git a/addons/addon-image/src/IIPHandler.ts b/addons/addon-image/src/IIPHandler.ts index 179545812..054e7a18a 100644 --- a/addons/addon-image/src/IIPHandler.ts +++ b/addons/addon-image/src/IIPHandler.ts @@ -204,7 +204,7 @@ export class IIPHandler implements IOscHandler, IResetHandler { .then(bm => { const id = this._storage.addImage(bm); if (this._aniManager && metrics.animated) { - this._aniManager.registerAnimation(id, blob as Blob, metrics.mime, w, h); + this._aniManager.registerAnimation(id, blob as Blob, metrics, w, h); } return true; }) diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index 590222f92..50994877b 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -70,9 +70,10 @@ const DEFAULT_OPTIONS: IImageAddonOptions = { showPlaceholder: true, iipSupport: true, iipSizeLimit: 33554432, - iipAnimation: true, // alpha kittySupport: true, - kittySizeLimit: 33554432 + kittySizeLimit: 33554432, + animationSupport: true, // alpha + animationPixelLimit: 4194304 // limit to 2048 * 2048 pixels }; // max palette size supported by the sixel lib (compile time setting) @@ -147,7 +148,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi { terminal.options.windowOptions = windowOps; } - if (this._opts.iipAnimation && typeof ImageDecoder === 'function') { + if (this._opts.animationSupport && typeof ImageDecoder === 'function') { this._aniManager = new AnimationManager(this._storage, this._renderer, terminal); this._disposeLater(terminal.onRender(range => this._aniManager!.viewportUpdated(range))); } diff --git a/addons/addon-image/src/Types.ts b/addons/addon-image/src/Types.ts index a8e110cca..229c335a9 100644 --- a/addons/addon-image/src/Types.ts +++ b/addons/addon-image/src/Types.ts @@ -39,9 +39,10 @@ export interface IImageAddonOptions { sixelSizeLimit: number; iipSupport: boolean; iipSizeLimit: number; - iipAnimation: boolean; kittySupport: boolean; kittySizeLimit: number; + animationSupport: boolean; + animationPixelLimit: number; } export interface IResetHandler { diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index 29e0d3a68..69c9436fc 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -24,9 +24,10 @@ export interface IImageAddonOptions { sixelSizeLimit: number; iipSupport: boolean; iipSizeLimit: number; - iipAnimation: boolean; kittySupport: boolean; kittySizeLimit: number; + animationSupport: boolean; + animationPixelLimit: number; } // eslint-disable-next-line @@ -138,9 +139,10 @@ test.describe('ImageAddon', () => { showPlaceholder: true, iipSupport: true, iipSizeLimit: 33554432, - iipAnimation: true, // alpha kittySupport: true, - kittySizeLimit: 33554432 + kittySizeLimit: 33554432, + animationSupport: true, + animationPixelLimit: 4194304 }; deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS); }); @@ -156,9 +158,10 @@ test.describe('ImageAddon', () => { showPlaceholder: false, iipSupport: false, iipSizeLimit: 1000, - iipAnimation: true, // alpha kittySupport: false, - kittySizeLimit: 1000 + kittySizeLimit: 1000, + animationSupport: false, + animationPixelLimit: 1000 }; await ctx.page.evaluate(opts => { (window as any).imageAddonCustom = new ImageAddon(opts.opts);