Files
mol 263cb5ef03
Some checks failed
continuous-integration/drone/push Build is failing
test
2024-07-06 22:23:31 +08:00

562 lines
22 KiB
JavaScript

"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;