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,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;
}