test
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
mol
2024-07-06 22:23:31 +08:00
parent 08173d8497
commit 263cb5ef03
1663 changed files with 526884 additions and 0 deletions

View File

@ -0,0 +1,19 @@
Copyright (c) 2019, 2020, 2021, 2022, 2023 The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2019 Joerg Breitbart.
* @license MIT
*/
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Copyright (c) 2021 Joerg Breitbart.
* @license MIT
*/
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/

View File

@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IIPHandler = void 0;
const ImageRenderer_1 = require("./ImageRenderer");
const ImageStorage_1 = require("./ImageStorage");
const Base64Decoder_wasm_1 = require("xterm-wasm-parts/lib/base64/Base64Decoder.wasm");
const IIPHeaderParser_1 = require("./IIPHeaderParser");
const IIPMetrics_1 = require("./IIPMetrics");
// limit hold memory in base64 decoder
const KEEP_DATA = 4194304;
// default IIP header values
const DEFAULT_HEADER = {
name: 'Unnamed file',
size: 0,
width: 'auto',
height: 'auto',
preserveAspectRatio: 1,
inline: 0
};
class IIPHandler {
constructor(_opts, _renderer, _storage, _coreTerminal) {
this._opts = _opts;
this._renderer = _renderer;
this._storage = _storage;
this._coreTerminal = _coreTerminal;
this._aborted = false;
this._hp = new IIPHeaderParser_1.HeaderParser();
this._header = DEFAULT_HEADER;
this._dec = new Base64Decoder_wasm_1.default(KEEP_DATA);
this._metrics = IIPMetrics_1.UNSUPPORTED_TYPE;
}
reset() { }
start() {
this._aborted = false;
this._header = DEFAULT_HEADER;
this._metrics = IIPMetrics_1.UNSUPPORTED_TYPE;
this._hp.reset();
}
put(data, start, end) {
if (this._aborted)
return;
if (this._hp.state === 4 /* HeaderState.END */) {
if (this._dec.put(data, start, end)) {
this._dec.release();
this._aborted = true;
}
}
else {
const dataPos = this._hp.parse(data, start, end);
if (dataPos === -1) {
this._aborted = true;
return;
}
if (dataPos > 0) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
this._aborted = true;
return;
}
this._dec.init(this._header.size);
if (this._dec.put(data, dataPos, end)) {
this._dec.release();
this._aborted = true;
}
}
}
}
end(success) {
if (this._aborted)
return true;
let w = 0;
let h = 0;
// early exit condition chain
let cond = true;
if (cond = success) {
if (cond = !this._dec.end()) {
this._metrics = (0, IIPMetrics_1.imageType)(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._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;
}
}
}
}
if (!cond) {
this._dec.release();
return true;
}
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
this._dec.release();
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise(r => {
img.addEventListener('load', () => {
var _a;
URL.revokeObjectURL(url);
const canvas = ImageRenderer_1.ImageRenderer.createCanvas(window.document, w, h);
(_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(img, 0, 0, w, h);
this._storage.addImage(canvas);
r(true);
});
img.src = url;
// sanity measure to avoid terminal blocking from dangling promise
// happens from corrupt data (onload never gets fired)
setTimeout(() => r(true), 1000);
});
}
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
.then(bm => {
this._storage.addImage(bm);
return true;
});
}
_resize(w, h) {
var _a, _b, _c, _d;
const cw = ((_a = this._renderer.dimensions) === null || _a === void 0 ? void 0 : _a.css.cell.width) || ImageStorage_1.CELL_SIZE_DEFAULT.width;
const ch = ((_b = this._renderer.dimensions) === null || _b === void 0 ? void 0 : _b.css.cell.height) || ImageStorage_1.CELL_SIZE_DEFAULT.height;
const width = ((_c = this._renderer.dimensions) === null || _c === void 0 ? void 0 : _c.css.canvas.width) || cw * this._coreTerminal.cols;
const height = ((_d = this._renderer.dimensions) === null || _d === void 0 ? void 0 : _d.css.canvas.height) || ch * this._coreTerminal.rows;
const rw = this._dim(this._header.width, width, cw);
const rh = this._dim(this._header.height, height, ch);
if (!rw && !rh) {
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 f = Math.min(wf, hf);
return f < 1 ? [w * f, h * f] : [w, h];
}
return !rw
? [w * rh / h, rh]
: this._header.preserveAspectRatio || !rw || !rh
? [rw, h * rw / w] : [rw, rh];
}
_dim(s, total, cdim) {
if (s === 'auto')
return 0;
if (s.endsWith('%'))
return parseInt(s.slice(0, -1)) * total / 100;
if (s.endsWith('px'))
return parseInt(s.slice(0, -2));
return parseInt(s) * cdim;
}
}
exports.IIPHandler = IIPHandler;

View File

@ -0,0 +1,155 @@
"use strict";
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.HeaderParser = void 0;
// field value decoders
// ASCII bytes to string
function toStr(data) {
let s = '';
for (let i = 0; i < data.length; ++i) {
s += String.fromCharCode(data[i]);
}
return s;
}
// digits to integer
function toInt(data) {
let v = 0;
for (let i = 0; i < data.length; ++i) {
if (data[i] < 48 || data[i] > 57) {
throw new Error('illegal char');
}
v = v * 10 + data[i] - 48;
}
return v;
}
// check for correct size entry
function toSize(data) {
const v = toStr(data);
if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
throw new Error('illegal size');
}
return v;
}
// name is base64 encoded utf-8
function toName(data) {
if (typeof Buffer !== 'undefined') {
return Buffer.from(toStr(data), 'base64').toString();
}
const bs = atob(toStr(data));
const b = new Uint8Array(bs.length);
for (let i = 0; i < b.length; ++i) {
b[i] = bs.charCodeAt(i);
}
return new TextDecoder().decode(b);
}
const DECODERS = {
inline: toInt,
size: toInt,
name: toName,
width: toSize,
height: toSize,
preserveAspectRatio: toInt
};
const FILE_MARKER = [70, 105, 108, 101];
const MAX_FIELDCHARS = 1024;
class HeaderParser {
constructor() {
this.state = 0 /* HeaderState.START */;
this._buffer = new Uint32Array(MAX_FIELDCHARS);
this._position = 0;
this._key = '';
this.fields = {};
}
reset() {
this._buffer.fill(0);
this.state = 0 /* HeaderState.START */;
this._position = 0;
this.fields = {};
this._key = '';
}
parse(data, start, end) {
let state = this.state;
let pos = this._position;
const buffer = this._buffer;
if (state === 1 /* HeaderState.ABORT */ || state === 4 /* HeaderState.END */)
return -1;
if (state === 0 /* HeaderState.START */ && pos > 6)
return -1;
for (let i = start; i < end; ++i) {
const c = data[i];
switch (c) {
case 59: // ;
if (!this._storeValue(pos))
return this._a();
state = 2 /* HeaderState.KEY */;
pos = 0;
break;
case 61: // =
if (state === 0 /* HeaderState.START */) {
for (let k = 0; k < FILE_MARKER.length; ++k) {
if (buffer[k] !== FILE_MARKER[k])
return this._a();
}
state = 2 /* HeaderState.KEY */;
pos = 0;
}
else if (state === 2 /* HeaderState.KEY */) {
if (!this._storeKey(pos))
return this._a();
state = 3 /* HeaderState.VALUE */;
pos = 0;
}
else if (state === 3 /* HeaderState.VALUE */) {
if (pos >= MAX_FIELDCHARS)
return this._a();
buffer[pos++] = c;
}
break;
case 58: // :
if (state === 3 /* HeaderState.VALUE */) {
if (!this._storeValue(pos))
return this._a();
}
this.state = 4 /* HeaderState.END */;
return i + 1;
default:
if (pos >= MAX_FIELDCHARS)
return this._a();
buffer[pos++] = c;
}
}
this.state = state;
this._position = pos;
return -2;
}
_a() {
this.state = 1 /* HeaderState.ABORT */;
return -1;
}
_storeKey(pos) {
const k = toStr(this._buffer.subarray(0, pos));
if (k) {
this._key = k;
this.fields[k] = null;
return true;
}
return false;
}
_storeValue(pos) {
if (this._key) {
try {
const v = this._buffer.slice(0, pos);
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
}
catch (e) {
return false;
}
return true;
}
return false;
}
}
exports.HeaderParser = HeaderParser;

View File

@ -0,0 +1,136 @@
"use strict";
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const IIPHeaderParser_1 = require("./IIPHeaderParser");
const CASES = [
['File=size=123456;name=dGVzdA==:', { name: 'test', size: 123456 }],
['File=size=123456;name=dGVzdA:', { name: 'test', size: 123456 }],
// utf-8 encoding in name
['File=size=123456;name=w7xtbMOkdXTDnw==:', { name: 'ümläutß', size: 123456 }],
['File=size=123456;name=w7xtbMOkdXTDnw:', { name: 'ümläutß', size: 123456 }],
// full header spec
[
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{
inline: 1,
width: '10px',
height: '20%',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
],
[
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{
inline: 1,
width: 'auto',
height: '20',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
]
];
function fromBs(bs) {
const r = new Uint32Array(bs.length);
for (let i = 0; i < r.length; ++i)
r[i] = bs.charCodeAt(i);
return r;
}
describe('IIPHeaderParser', () => {
it('at once', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
for (const example of CASES) {
hp.reset();
const inp = fromBs(example[0]);
const res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, example[1]);
}
});
it('bytewise', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
for (const example of CASES) {
hp.reset();
const inp = fromBs(example[0]);
let pos = 0;
let res = -2;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
chai_1.assert.strictEqual(res, 1);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, example[1]);
}
});
it('no File= starter', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
let inp = fromBs('size=123456;name=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty key - error', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
let inp = fromBs('File=size=123456;=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty size value - set to 0', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
let inp = fromBs('File=size=;name=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, { name: 'test', size: 0 });
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty name value - set to empty string', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
let inp = fromBs('File=size=123456;name=:');
let res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, { name: '', size: 123456 });
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty size value - error', () => {
const hp = new IIPHeaderParser_1.HeaderParser();
let inp = fromBs('File=inline=1;width=;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:');
let res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
chai_1.assert.strictEqual(res, inp.length);
chai_1.assert.strictEqual(hp.state, 4 /* HeaderState.END */);
chai_1.assert.deepEqual(hp.fields, CASES[0][1]);
});
});

View File

@ -0,0 +1,70 @@
"use strict";
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.imageType = exports.UNSUPPORTED_TYPE = void 0;
exports.UNSUPPORTED_TYPE = {
mime: 'unsupported',
width: 0,
height: 0
};
function imageType(d) {
if (d.length < 24) {
return exports.UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF E0 xx xx JFIF or FF D8 FF E1 xx xx Exif 00 00
if ((d32[0] === 0xE0FFD8FF || d32[0] === 0xE1FFD8FF)
&& ((d[6] === 0x4a && d[7] === 0x46 && d[8] === 0x49 && d[9] === 0x46)
|| (d[6] === 0x45 && d[7] === 0x78 && d[8] === 0x69 && d[9] === 0x66))) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
return exports.UNSUPPORTED_TYPE;
}
exports.imageType = imageType;
function jpgSize(d) {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}

View File

@ -0,0 +1,37 @@
"use strict";
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const IIPMetrics_1 = require("./IIPMetrics");
const fs = require('fs');
const TEST_IMAGES = [
['w3c_home_256.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_256.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_256.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home_2.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_2.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_2.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home_animation.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_gray.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_gray.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_gray.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home.png', { mime: 'image/png', width: 72, height: 48 }],
['spinfox.png', { mime: 'image/png', width: 148, height: 148 }],
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }],
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }],
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }]
];
describe('IIPMetrics', () => {
it('bunch of testimages', () => {
for (let i = 0; i < TEST_IMAGES.length; ++i) {
const imageData = fs.readFileSync('./addons/addon-image/fixture/testimages/' + TEST_IMAGES[i][0]);
chai_1.assert.deepStrictEqual((0, IIPMetrics_1.imageType)(imageData), TEST_IMAGES[i][1]);
}
});
});

View File

@ -0,0 +1,260 @@
"use strict";
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageAddon = void 0;
const IIPHandler_1 = require("./IIPHandler");
const ImageRenderer_1 = require("./ImageRenderer");
const ImageStorage_1 = require("./ImageStorage");
const SixelHandler_1 = require("./SixelHandler");
// default values of addon ctor options
const DEFAULT_OPTIONS = {
enableSizeReports: true,
pixelLimit: 16777216,
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 256,
sixelSizeLimit: 25000000,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
};
// max palette size supported by the sixel lib (compile time setting)
const MAX_SIXEL_PALETTE_SIZE = 4096;
class ImageAddon {
constructor(opts) {
this._disposables = [];
this._handlers = new Map();
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
this._defaultOpts = Object.assign({}, DEFAULT_OPTIONS, opts);
}
dispose() {
for (const obj of this._disposables) {
obj.dispose();
}
this._disposables.length = 0;
this._handlers.clear();
}
_disposeLater(...args) {
for (const obj of args) {
this._disposables.push(obj);
}
}
activate(terminal) {
this._terminal = terminal;
// internal data structures
this._renderer = new ImageRenderer_1.ImageRenderer(terminal);
this._storage = new ImageStorage_1.ImageStorage(terminal, this._renderer, this._opts);
// enable size reports
if (this._opts.enableSizeReports) {
// const windowOptions = terminal.getOption('windowOptions');
// windowOptions.getWinSizePixels = true;
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
const windowOps = terminal.options.windowOptions || {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
windowOps.getWinSizeChars = true;
terminal.options.windowOptions = windowOps;
}
this._disposeLater(this._renderer, this._storage,
// DECSET/DECRST/DA1/XTSMGRAPHICS handlers
terminal.parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this._decset(params)), terminal.parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this._decrst(params)), terminal.parser.registerCsiHandler({ final: 'c' }, params => this._da1(params)), terminal.parser.registerCsiHandler({ prefix: '?', final: 'S' }, params => this._xtermGraphicsAttributes(params)),
// render hook
terminal.onRender(range => { var _a; return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.render(range); }),
/**
* reset handlers covered:
* - DECSTR
* - RIS
* - Terminal.reset()
*/
terminal.parser.registerCsiHandler({ intermediates: '!', final: 'p' }, () => this.reset()), terminal.parser.registerEscHandler({ final: 'c' }, () => this.reset()), terminal._core._inputHandler.onRequestReset(() => this.reset()),
// wipe canvas and delete alternate images on buffer switch
terminal.buffer.onBufferChange(() => { var _a; return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.wipeAlternate(); }),
// extend images to the right on resize
terminal.onResize(metrics => { var _a; return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.viewportResize(metrics); }));
// SIXEL handler
if (this._opts.sixelSupport) {
const sixelHandler = new SixelHandler_1.SixelHandler(this._opts, this._storage, terminal);
this._handlers.set('sixel', sixelHandler);
this._disposeLater(terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler));
}
// iTerm IIP handler
if (this._opts.iipSupport) {
const iipHandler = new IIPHandler_1.IIPHandler(this._opts, this._renderer, this._storage, terminal);
this._handlers.set('iip', iipHandler);
this._disposeLater(terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler));
}
}
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
reset() {
var _a;
// reset options customizable by sequences to defaults
this._opts.sixelScrolling = this._defaultOpts.sixelScrolling;
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
// also clear image storage
(_a = this._storage) === null || _a === void 0 ? void 0 : _a.reset();
// reset protocol handlers
for (const handler of this._handlers.values()) {
handler.reset();
}
return false;
}
get storageLimit() {
var _a;
return ((_a = this._storage) === null || _a === void 0 ? void 0 : _a.getLimit()) || -1;
}
set storageLimit(limit) {
var _a;
(_a = this._storage) === null || _a === void 0 ? void 0 : _a.setLimit(limit);
this._opts.storageLimit = limit;
}
get storageUsage() {
if (this._storage) {
return this._storage.getUsage();
}
return -1;
}
get showPlaceholder() {
return this._opts.showPlaceholder;
}
set showPlaceholder(value) {
var _a;
this._opts.showPlaceholder = value;
(_a = this._renderer) === null || _a === void 0 ? void 0 : _a.showPlaceholder(value);
}
getImageAtBufferCell(x, y) {
var _a;
return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.getImageAtBufferCell(x, y);
}
extractTileAtBufferCell(x, y) {
var _a;
return (_a = this._storage) === null || _a === void 0 ? void 0 : _a.extractTileAtBufferCell(x, y);
}
_report(s) {
var _a;
(_a = this._terminal) === null || _a === void 0 ? void 0 : _a._core.coreService.triggerDataEvent(s);
}
_decset(params) {
for (let i = 0; i < params.length; ++i) {
switch (params[i]) {
case 80:
this._opts.sixelScrolling = false;
break;
}
}
return false;
}
_decrst(params) {
for (let i = 0; i < params.length; ++i) {
switch (params[i]) {
case 80:
this._opts.sixelScrolling = true;
break;
}
}
return false;
}
// overload DA to return something more appropriate
_da1(params) {
if (params[0]) {
return true;
}
// reported features:
// 62 - VT220
// 4 - SIXEL support
// 9 - charsets
// 22 - ANSI colors
if (this._opts.sixelSupport) {
this._report(`\x1b[?62;4;9;22c`);
return true;
}
return false;
}
/**
* Implementation of xterm's graphics attribute sequence.
*
* Supported features:
* - read/change palette limits (max 4096 by sixel lib)
* - read SIXEL canvas geometry (reports current window canvas or
* squared pixelLimit if canvas > pixel limit)
*
* Everything else is deactivated.
*/
_xtermGraphicsAttributes(params) {
var _a, _b, _c, _d, _e, _f;
if (params.length < 2) {
return true;
}
if (params[0] === 1 /* GaItem.COLORS */) {
switch (params[1]) {
case 1 /* GaAction.READ */:
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${this._opts.sixelPaletteLimit}S`);
return true;
case 2 /* GaAction.SET_DEFAULT */:
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${this._opts.sixelPaletteLimit}S`);
// also reset protocol handlers for now
for (const handler of this._handlers.values()) {
handler.reset();
}
return true;
case 3 /* GaAction.SET */:
if (params.length > 2 && !(params[2] instanceof Array) && params[2] <= MAX_SIXEL_PALETTE_SIZE) {
this._opts.sixelPaletteLimit = params[2];
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${this._opts.sixelPaletteLimit}S`);
}
else {
this._report(`\x1b[?${params[0]};${2 /* GaStatus.ACTION_ERROR */}S`);
}
return true;
case 4 /* GaAction.READ_MAX */:
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${MAX_SIXEL_PALETTE_SIZE}S`);
return true;
default:
this._report(`\x1b[?${params[0]};${2 /* GaStatus.ACTION_ERROR */}S`);
return true;
}
}
if (params[0] === 2 /* GaItem.SIXEL_GEO */) {
switch (params[1]) {
// we only implement read and read_max here
case 1 /* GaAction.READ */:
let width = (_b = (_a = this._renderer) === null || _a === void 0 ? void 0 : _a.dimensions) === null || _b === void 0 ? void 0 : _b.css.canvas.width;
let height = (_d = (_c = this._renderer) === null || _c === void 0 ? void 0 : _c.dimensions) === null || _d === void 0 ? void 0 : _d.css.canvas.height;
if (!width || !height) {
// for some reason we have no working image renderer
// --> fallback to default cell size
const cellSize = ImageStorage_1.CELL_SIZE_DEFAULT;
width = (((_e = this._terminal) === null || _e === void 0 ? void 0 : _e.cols) || 80) * cellSize.width;
height = (((_f = this._terminal) === null || _f === void 0 ? void 0 : _f.rows) || 24) * cellSize.height;
}
if (width * height < this._opts.pixelLimit) {
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${width.toFixed(0)};${height.toFixed(0)}S`);
}
else {
// if we overflow pixelLimit report that squared instead
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${x};${x}S`);
}
return true;
case 4 /* GaAction.READ_MAX */:
// read_max returns pixelLimit as square area
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
this._report(`\x1b[?${params[0]};${0 /* GaStatus.SUCCESS */};${x};${x}S`);
return true;
default:
this._report(`\x1b[?${params[0]};${2 /* GaStatus.ACTION_ERROR */}S`);
return true;
}
}
// exit with error on ReGIS or any other requests
this._report(`\x1b[?${params[0]};${1 /* GaStatus.ITEM_ERROR */}S`);
return true;
}
}
exports.ImageAddon = ImageAddon;

View File

@ -0,0 +1,329 @@
"use strict";
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageRenderer = void 0;
const Colors_1 = require("sixel/lib/Colors");
const Lifecycle_1 = require("common/Lifecycle");
const PLACEHOLDER_LENGTH = 4096;
const PLACEHOLDER_HEIGHT = 24;
/**
* ImageRenderer - terminal frontend extension:
* - provide primitives for canvas, ImageData, Bitmap (static)
* - add canvas layer to DOM (browser only for now)
* - draw image tiles onRender
*/
class ImageRenderer extends Lifecycle_1.Disposable {
// drawing primitive - canvas
static createCanvas(localDocument, width, height) {
/**
* 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
static createImageData(ctx, width, height, buffer) {
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
static createImageBitmap(img) {
if (typeof createImageBitmap !== 'function') {
return Promise.resolve(undefined);
}
return createImageBitmap(img);
}
constructor(_terminal) {
super();
this._terminal = _terminal;
this._optionsRefresh = this.register(new Lifecycle_1.MutableDisposable());
this._oldOpen = this._terminal._core.open;
this._terminal._core.open = (parent) => {
var _a;
(_a = this._oldOpen) === null || _a === void 0 ? void 0 : _a.call(this._terminal._core, parent);
this._open();
};
if (this._terminal._core.screenElement) {
this._open();
}
// hack to spot fontSize changes
this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => {
var _a;
if (option === 'fontSize') {
this.rescaleCanvas();
(_a = this._renderService) === null || _a === void 0 ? void 0 : _a.refreshRows(0, this._terminal.rows);
}
});
this.register((0, Lifecycle_1.toDisposable)(() => {
var _a;
this.removeLayerFromDom();
if (this._terminal._core && this._oldOpen) {
this._terminal._core.open = this._oldOpen;
this._oldOpen = undefined;
}
if (this._renderService && this._oldSetRenderer) {
this._renderService.setRenderer = this._oldSetRenderer;
this._oldSetRenderer = undefined;
}
this._renderService = undefined;
this.canvas = undefined;
this._ctx = undefined;
(_a = this._placeholderBitmap) === null || _a === void 0 ? void 0 : _a.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}));
}
/**
* Enable the placeholder.
*/
showPlaceholder(value) {
var _a, _b;
if (value) {
if (!this._placeholder && this.cellSize.height !== -1) {
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
}
}
else {
(_a = this._placeholderBitmap) === null || _a === void 0 ? void 0 : _a.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}
(_b = this._renderService) === null || _b === void 0 ? void 0 : _b.refreshRows(0, this._terminal.rows);
}
/**
* Dimensions of the terminal.
* Forwarded from internal render service.
*/
get dimensions() {
var _a;
return (_a = this._renderService) === null || _a === void 0 ? void 0 : _a.dimensions;
}
/**
* Current cell size (float).
*/
get cellSize() {
var _a, _b;
return {
width: ((_a = this.dimensions) === null || _a === void 0 ? void 0 : _a.css.cell.width) || -1,
height: ((_b = this.dimensions) === null || _b === void 0 ? void 0 : _b.css.cell.height) || -1
};
}
/**
* Clear a region of the image layer canvas.
*/
clearLines(start, end) {
var _a, _b, _c, _d;
(_a = this._ctx) === null || _a === void 0 ? void 0 : _a.clearRect(0, start * (((_b = this.dimensions) === null || _b === void 0 ? void 0 : _b.css.cell.height) || 0), ((_c = this.dimensions) === null || _c === void 0 ? void 0 : _c.css.canvas.width) || 0, (++end - start) * (((_d = this.dimensions) === null || _d === void 0 ? void 0 : _d.css.cell.height) || 0));
}
/**
* Clear whole image canvas.
*/
clearAll() {
var _a, _b, _c;
(_a = this._ctx) === null || _a === void 0 ? void 0 : _a.clearRect(0, 0, ((_b = this.canvas) === null || _b === void 0 ? void 0 : _b.width) || 0, ((_c = this.canvas) === null || _c === void 0 ? void 0 : _c.height) || 0);
}
/**
* Draw neighboring tiles on the image layer canvas.
*/
draw(imgSpec, tileId, col, row, count = 1) {
if (!this._ctx) {
return;
}
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 dx = col * width;
const dy = row * height;
// safari bug: never access image source out of bounds
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
// Floor all pixel offsets to get stable tile mapping without any overflows.
// 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).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
this._ctx.drawImage(img, Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight), Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight));
}
/**
* Extract a single tile from an image.
*/
extractTile(imgSpec, tileId) {
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.
*/
drawPlaceholder(col, row, count = 1) {
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 (!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, (row * height) % 2 ? 0 : 1, // needs %2 offset correction
width * count, height, col * width, row * height, width * count, height);
}
}
/**
* Rescale image layer canvas if needed.
* Checked once from `ImageStorage.render`.
*/
rescaleCanvas() {
if (!this.canvas) {
return;
}
if (this.canvas.width !== this.dimensions.css.canvas.width || this.canvas.height !== this.dimensions.css.canvas.height) {
this.canvas.width = this.dimensions.css.canvas.width || 0;
this.canvas.height = this.dimensions.css.canvas.height || 0;
}
}
/**
* Rescale image in storage if needed.
*/
_rescaleImage(spec, currentWidth, currentHeight) {
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
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;
}
}
/**
* Lazy init for the renderer.
*/
_open() {
this._renderService = this._terminal._core._renderService;
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
this._renderService.setRenderer = (renderer) => {
var _a;
this.removeLayerFromDom();
(_a = this._oldSetRenderer) === null || _a === void 0 ? void 0 : _a.call(this._renderService, renderer);
};
}
insertLayerToDom() {
var _a, _b;
// 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, ((_a = this.dimensions) === null || _a === void 0 ? void 0 : _a.css.canvas.width) || 0, ((_b = this.dimensions) === null || _b === void 0 ? void 0 : _b.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');
}
}
removeLayerFromDom() {
if (this.canvas) {
this._ctx = undefined;
this.canvas.remove();
this.canvas = undefined;
}
}
_createPlaceHolder(height = PLACEHOLDER_HEIGHT) {
var _a;
(_a = this._placeholderBitmap) === null || _a === void 0 ? void 0 : _a.close();
this._placeholderBitmap = undefined;
// create blueprint to fill placeholder with
const bWidth = 32; // must be 2^n
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);
const d32 = new Uint32Array(imgData.data.buffer);
const black = (0, Colors_1.toRGBA8888)(0, 0, 0);
const white = (0, Colors_1.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);
// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || 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);
}
get document() {
var _a;
return (_a = this._terminal._core._coreBrowserService) === null || _a === void 0 ? void 0 : _a.window.document;
}
}
exports.ImageRenderer = ImageRenderer;

View File

@ -0,0 +1,562 @@
"use strict";
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageStorage = exports.CELL_SIZE_DEFAULT = void 0;
const ImageRenderer_1 = require("./ImageRenderer");
// fallback default cell size
exports.CELL_SIZE_DEFAULT = {
width: 7,
height: 14
};
/**
* Extend extended attribute to also hold image tile information.
*
* Object definition is copied from base repo to fully mimick its behavior.
* Image data is added as additional public properties `imageId` and `tileId`.
*/
class ExtendedAttrsImage {
get ext() {
if (this._urlId) {
return ((this._ext & ~469762048 /* ExtFlags.UNDERLINE_STYLE */) |
(this.underlineStyle << 26));
}
return this._ext;
}
set ext(value) { this._ext = value; }
get underlineStyle() {
// Always return the URL style if it has one
if (this._urlId) {
return 5 /* UnderlineStyle.DASHED */;
}
return (this._ext & 469762048 /* ExtFlags.UNDERLINE_STYLE */) >> 26;
}
set underlineStyle(value) {
this._ext &= ~469762048 /* ExtFlags.UNDERLINE_STYLE */;
this._ext |= (value << 26) & 469762048 /* ExtFlags.UNDERLINE_STYLE */;
}
get underlineColor() {
return this._ext & (50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
}
set underlineColor(value) {
this._ext &= ~(50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
this._ext |= value & (50331648 /* Attributes.CM_MASK */ | 16777215 /* Attributes.RGB_MASK */);
}
get underlineVariantOffset() {
const val = (this._ext & 3758096384 /* ExtFlags.VARIANT_OFFSET */) >> 29;
if (val < 0) {
return val ^ 0xFFFFFFF8;
}
return val;
}
set underlineVariantOffset(value) {
this._ext &= ~3758096384 /* ExtFlags.VARIANT_OFFSET */;
this._ext |= (value << 29) & 3758096384 /* ExtFlags.VARIANT_OFFSET */;
}
get urlId() {
return this._urlId;
}
set urlId(value) {
this._urlId = value;
}
constructor(ext = 0, urlId = 0, imageId = -1, tileId = -1) {
this.imageId = imageId;
this.tileId = tileId;
this._ext = 0;
this._urlId = 0;
this._ext = ext;
this._urlId = urlId;
}
clone() {
/**
* Technically we dont need a clone variant of ExtendedAttrsImage,
* as we never clone a cell holding image data.
* Note: Clone is only meant to be used by the InputHandler for
* sticky attributes, which is never the case for image data.
* We still provide a proper clone method to reflect the full ext attr
* state in case there are future use cases for clone.
*/
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
}
isEmpty() {
return this.underlineStyle === 0 /* UnderlineStyle.NONE */ && this._urlId === 0 && this.imageId === -1;
}
}
const EMPTY_ATTRS = new ExtendedAttrsImage();
/**
* ImageStorage - extension of CoreTerminal:
* - hold image data
* - write/read image data to/from buffer
*
* TODO: image composition for overwrites
*/
class ImageStorage {
constructor(_terminal, _renderer, _opts) {
this._terminal = _terminal;
this._renderer = _renderer;
this._opts = _opts;
// storage
this._images = new Map();
// last used id
this._lastId = 0;
// last evicted id
this._lowestId = 0;
// whether a full clear happened before
this._fullyCleared = false;
// whether render should do a full clear
this._needsFullClear = false;
// hard limit of stored pixels (fallback limit of 10 MB)
this._pixelLimit = 2500000;
try {
this.setLimit(this._opts.storageLimit);
}
catch (e) {
console.error(e.message);
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
}
this._viewportMetrics = {
cols: this._terminal.cols,
rows: this._terminal.rows
};
}
dispose() {
this.reset();
}
reset() {
var _a;
for (const spec of this._images.values()) {
(_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
}
// NOTE: marker.dispose above already calls ImageBitmap.close
// therefore we can just wipe the map here
this._images.clear();
this._renderer.clearAll();
}
getLimit() {
return this._pixelLimit * 4 / 1000000;
}
setLimit(value) {
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);
}
getUsage() {
return this._getStoredPixels() * 4 / 1000000;
}
_getStoredPixels() {
let storedPixels = 0;
for (const spec of this._images.values()) {
if (spec.orig) {
storedPixels += spec.orig.width * spec.orig.height;
if (spec.actual && spec.actual !== spec.orig) {
storedPixels += spec.actual.width * spec.actual.height;
}
}
}
return storedPixels;
}
_delImg(id) {
const spec = this._images.get(id);
this._images.delete(id);
// FIXME: really ugly workaround to get bitmaps deallocated :(
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
spec.orig.close();
}
}
/**
* Wipe canvas and images on alternate buffer.
*/
wipeAlternate() {
var _a;
// remove all alternate tagged images
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.bufferType === 'alternate') {
(_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
// mark canvas to be wiped on next render
this._needsFullClear = true;
this._fullyCleared = false;
}
/**
* Only advance text cursor.
* This is an edge case from empty sixels carrying only a height but no pixels.
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
*/
advanceCursor(height) {
if (this._opts.sixelScrolling) {
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = exports.CELL_SIZE_DEFAULT;
}
const rows = Math.ceil(height / cellSize.height);
for (let i = 1; i < rows; ++i) {
this._terminal._core._inputHandler.lineFeed();
}
}
}
/**
* Method to add an image to the storage.
*/
addImage(img) {
var _a;
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
// calc rows x cols needed to display the image
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = exports.CELL_SIZE_DEFAULT;
}
const cols = Math.ceil(img.width / cellSize.width);
const rows = Math.ceil(img.height / cellSize.height);
const imageId = ++this._lastId;
const buffer = this._terminal._core.buffer;
const termCols = this._terminal.cols;
const termRows = this._terminal.rows;
const originX = buffer.x;
const originY = buffer.y;
let offset = originX;
let tileCount = 0;
if (!this._opts.sixelScrolling) {
buffer.x = 0;
buffer.y = 0;
offset = 0;
}
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(buffer.y + buffer.ybase);
for (let col = 0; col < cols; ++col) {
if (offset + col >= termCols)
break;
this._writeToCell(line, offset + col, imageId, row * cols + col);
tileCount++;
}
if (this._opts.sixelScrolling) {
if (row < rows - 1)
this._terminal._core._inputHandler.lineFeed();
}
else {
if (++buffer.y >= termRows)
break;
}
buffer.x = offset;
}
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
// cursor positioning modes
if (this._opts.sixelScrolling) {
buffer.x = offset;
}
else {
buffer.x = originX;
buffer.y = originY;
}
// deleted images with zero tile count
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.tileCount < 1) {
(_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
// eviction marker:
// delete the image when the marker gets disposed
const endMarker = this._terminal.registerMarker(0);
endMarker === null || endMarker === void 0 ? void 0 : endMarker.onDispose(() => {
const spec = this._images.get(imageId);
if (spec) {
this._delImg(imageId);
}
});
// since markers do not work on alternate for some reason,
// we evict images here manually
if (this._terminal.buffer.active.type === 'alternate') {
this._evictOnAlternate();
}
// create storage entry
const imgSpec = {
orig: img,
origCellSize: cellSize,
actual: img,
actualCellSize: Object.assign({}, cellSize),
marker: endMarker || undefined,
tileCount,
bufferType: this._terminal.buffer.active.type
};
// finally add the image
this._images.set(imageId, imgSpec);
}
/**
* Render method. Collects buffer information and triggers
* canvas updates.
*/
// TODO: Should we move this to the ImageRenderer?
render(range) {
// 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();
// exit early if we dont have any images to test for
if (!this._images.size) {
if (!this._fullyCleared) {
this._renderer.clearAll();
this._fullyCleared = true;
this._needsFullClear = false;
}
if (this._renderer.canvas) {
this._renderer.removeLayerFromDom();
}
return;
}
// buffer switches force a full clear
if (this._needsFullClear) {
this._renderer.clearAll();
this._fullyCleared = true;
this._needsFullClear = false;
}
const { start, end } = range;
const buffer = this._terminal._core.buffer;
const cols = this._terminal._core.cols;
// clear drawing area
this._renderer.clearLines(start, end);
// walk all cells in viewport and draw tiles found
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp);
if (!line)
return;
for (let col = 0; col < cols; ++col) {
if (line.getBg(col) & 268435456 /* BgFlags.HAS_EXTENDED */) {
let e = line._extendedAttrs[col] || EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
}
const imgSpec = this._images.get(imageId);
if (e.tileId !== -1) {
const startTile = e.tileId;
const startCol = col;
let count = 1;
/**
* merge tiles to the right into a single draw call, if:
* - not at end of line
* - cell has same image id
* - cell has consecutive tile id
*/
while (++col < cols
&& (line.getBg(col) & 268435456 /* BgFlags.HAS_EXTENDED */)
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
&& (e.imageId === imageId)
&& (e.tileId === startTile + count)) {
count++;
}
col--;
if (imgSpec) {
if (imgSpec.actual) {
this._renderer.draw(imgSpec, startTile, startCol, row, count);
}
}
else if (this._opts.showPlaceholder) {
this._renderer.drawPlaceholder(startCol, row, count);
}
this._fullyCleared = false;
}
}
}
}
}
viewportResize(metrics) {
var _a;
// exit early if we have nothing in storage
if (!this._images.size) {
this._viewportMetrics = metrics;
return;
}
// handle only viewport width enlargements, exit all other cases
// TODO: needs patch for tile counter
if (this._viewportMetrics.cols >= metrics.cols) {
this._viewportMetrics = metrics;
return;
}
// walk scrollbuffer at old col width to find all possible expansion matches
const buffer = this._terminal._core.buffer;
const rows = buffer.lines.length;
const oldCol = this._viewportMetrics.cols - 1;
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row);
if (line.getBg(oldCol) & 268435456 /* BgFlags.HAS_EXTENDED */) {
const e = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
}
const imgSpec = this._images.get(imageId);
if (!imgSpec) {
continue;
}
// found an image tile at oldCol, check if it qualifies for right exapansion
const tilesPerRow = Math.ceil((((_a = imgSpec.actual) === null || _a === void 0 ? void 0 : _a.width) || 0) / imgSpec.actualCellSize.width);
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
continue;
}
// expand only if right side is empty (nothing got wrapped from below)
let hasData = false;
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
if (line._data[rightCol * 3 /* Cell.SIZE */ + 0 /* Cell.CONTENT */] & 4194303 /* Content.HAS_CONTENT_MASK */) {
hasData = true;
break;
}
}
if (hasData) {
continue;
}
// do right expansion on terminal buffer
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
let lastTile = e.tileId;
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
this._writeToCell(line, expandCol, imageId, ++lastTile);
imgSpec.tileCount++;
}
}
}
// store new viewport metrics
this._viewportMetrics = metrics;
}
/**
* Retrieve original canvas at buffer position.
*/
getImageAtBufferCell(x, y) {
var _a, _b;
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y);
if (line && line.getBg(x) & 268435456 /* BgFlags.HAS_EXTENDED */) {
const e = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const orig = (_a = this._images.get(e.imageId)) === null || _a === void 0 ? void 0 : _a.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer_1.ImageRenderer.createCanvas(window.document, orig.width, orig.height);
(_b = canvas.getContext('2d')) === null || _b === void 0 ? void 0 : _b.drawImage(orig, 0, 0, orig.width, orig.height);
return canvas;
}
return orig;
}
}
}
/**
* Extract active single tile at buffer position.
*/
extractTileAtBufferCell(x, y) {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y);
if (line && line.getBg(x) & 268435456 /* BgFlags.HAS_EXTENDED */) {
const e = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
return this._renderer.extractTile(spec, e.tileId);
}
}
}
}
// 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
_evictOldest(room) {
var _a;
const used = this._getStoredPixels();
let current = used;
while (this._pixelLimit < current + room && this._images.size) {
const spec = this._images.get(++this._lowestId);
if (spec && spec.orig) {
current -= spec.orig.width * spec.orig.height;
if (spec.actual && spec.orig !== spec.actual) {
current -= spec.actual.width * spec.actual.height;
}
(_a = spec.marker) === null || _a === void 0 ? void 0 : _a.dispose();
this._delImg(this._lowestId);
}
}
return used - current;
}
_writeToCell(line, x, imageId, tileId) {
if (line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] & 268435456 /* BgFlags.HAS_EXTENDED */) {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
old.imageId = imageId;
old.tileId = tileId;
return;
}
// found a plain ExtendedAttrs instance, clone it to new entry
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return;
}
}
// fall-through: always create new ExtendedAttrsImage entry
line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] |= 268435456 /* BgFlags.HAS_EXTENDED */;
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
}
_evictOnAlternate() {
var _a, _b;
// nullify tile count of all images on alternate buffer
for (const spec of this._images.values()) {
if (spec.bufferType === 'alternate') {
spec.tileCount = 0;
}
}
// re-count tiles on whole buffer
const buffer = this._terminal._core.buffer;
for (let y = 0; y < this._terminal.rows; ++y) {
const line = buffer.lines.get(y);
if (!line) {
continue;
}
for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * 3 /* Cell.SIZE */ + 2 /* Cell.BG */] & 268435456 /* BgFlags.HAS_EXTENDED */) {
const imgId = (_a = line._extendedAttrs[x]) === null || _a === void 0 ? void 0 : _a.imageId;
if (imgId) {
const spec = this._images.get(imgId);
if (spec) {
spec.tileCount++;
}
}
}
}
}
// deleted images with zero tile count
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.bufferType === 'alternate' && !spec.tileCount) {
(_b = spec.marker) === null || _b === void 0 ? void 0 : _b.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
}
}
exports.ImageStorage = ImageStorage;

View File

@ -0,0 +1,139 @@
"use strict";
/**
* Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SixelHandler = void 0;
const Colors_1 = require("sixel/lib/Colors");
const ImageRenderer_1 = require("./ImageRenderer");
const Decoder_1 = require("sixel/lib/Decoder");
// always free decoder ressources after decoding if it exceeds this limit
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
// custom default palette: VT340 (lower 16 colors) + ANSI256 (up to 256) + zeroed (up to 4096)
const DEFAULT_PALETTE = Colors_1.PALETTE_ANSI_256;
DEFAULT_PALETTE.set(Colors_1.PALETTE_VT340_COLOR);
class SixelHandler {
constructor(_opts, _storage, _coreTerminal) {
this._opts = _opts;
this._storage = _storage;
this._coreTerminal = _coreTerminal;
this._size = 0;
this._aborted = false;
(0, Decoder_1.DecoderAsync)({
memoryLimit: this._opts.pixelLimit * 4,
palette: DEFAULT_PALETTE,
paletteLimit: this._opts.sixelPaletteLimit
}).then(d => this._dec = d);
}
reset() {
/**
* reset sixel decoder to defaults:
* - release all memory
* - nullify palette (4096)
* - apply default palette (256)
*/
if (this._dec) {
this._dec.release();
// FIXME: missing interface on decoder to nullify full palette
this._dec._palette.fill(0);
this._dec.init(0, DEFAULT_PALETTE, this._opts.sixelPaletteLimit);
}
}
hook(params) {
var _a;
this._size = 0;
this._aborted = false;
if (this._dec) {
const fillColor = params.params[1] === 1 ? 0 : extractActiveBg(this._coreTerminal._core._inputHandler._curAttrData, (_a = this._coreTerminal._core._themeService) === null || _a === void 0 ? void 0 : _a.colors);
this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
}
}
put(data, start, end) {
if (this._aborted || !this._dec) {
return;
}
this._size += end - start;
if (this._size > this._opts.sixelSizeLimit) {
console.warn(`SIXEL: too much data, aborting`);
this._aborted = true;
this._dec.release();
return;
}
try {
this._dec.decode(data, start, end);
}
catch (e) {
console.warn(`SIXEL: error while decoding image - ${e}`);
this._aborted = true;
this._dec.release();
}
}
unhook(success) {
var _a;
if (this._aborted || !success || !this._dec) {
return true;
}
const width = this._dec.width;
const height = this._dec.height;
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
if (!width || !height) {
if (height) {
this._storage.advanceCursor(height);
}
return true;
}
const canvas = ImageRenderer_1.ImageRenderer.createCanvas(undefined, width, height);
(_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
}
this._storage.addImage(canvas);
return true;
}
}
exports.SixelHandler = SixelHandler;
/**
* Some helpers to extract current terminal colors.
*/
// get currently active background color from terminal
// also respect INVERSE setting
function extractActiveBg(attr, colors) {
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);
}
else if (attr.isFgRGB()) {
const t = attr.constructor.toColorRGB(attr.getFgColor());
bg = (0, Colors_1.toRGBA8888)(...t);
}
else {
bg = convertLe(colors.ansi[attr.getFgColor()].rgba);
}
}
else {
if (attr.isBgDefault()) {
bg = convertLe(colors.background.rgba);
}
else if (attr.isBgRGB()) {
const t = attr.constructor.toColorRGB(attr.getBgColor());
bg = (0, Colors_1.toRGBA8888)(...t);
}
else {
bg = convertLe(colors.ansi[attr.getBgColor()].rgba);
}
}
return bg;
}
// rgba values on the color managers are always in BE, thus convert to LE
function convertLe(color) {
if (Colors_1.BIG_ENDIAN)
return color;
return (color & 0xFF) << 24 | (color >>> 8 & 0xFF) << 16 | (color >>> 16 & 0xFF) << 8 | color >>> 24 & 0xFF;
}

View File

@ -0,0 +1,31 @@
{
"name": "@xterm/addon-image",
"version": "0.9.0-beta.21",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/addon-image.js",
"types": "typings/addon-image.d.ts",
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image",
"license": "MIT",
"keywords": [
"terminal",
"image",
"sixel",
"xterm",
"xterm.js"
],
"scripts": {
"prepackage": "../../node_modules/.bin/tsc -p .",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"@xterm/xterm": "^5.2.0"
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
}
}