@xterm/addon-image 0.7.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +231 -0
- package/lib/addon-image.js +3 -0
- package/lib/addon-image.js.LICENSE.txt +24 -0
- package/lib/addon-image.js.map +1 -0
- package/out/IIPHandler.js +148 -0
- package/out/IIPHandler.js.map +1 -0
- package/out/IIPHeaderParser.js +156 -0
- package/out/IIPHeaderParser.js.map +1 -0
- package/out/IIPHeaderParser.test.js +137 -0
- package/out/IIPHeaderParser.test.js.map +1 -0
- package/out/IIPMetrics.js +71 -0
- package/out/IIPMetrics.js.map +1 -0
- package/out/IIPMetrics.test.js +38 -0
- package/out/IIPMetrics.test.js.map +1 -0
- package/out/ImageAddon.js +261 -0
- package/out/ImageAddon.js.map +1 -0
- package/out/ImageRenderer.js +330 -0
- package/out/ImageRenderer.js.map +1 -0
- package/out/ImageStorage.js +552 -0
- package/out/ImageStorage.js.map +1 -0
- package/out/SixelHandler.js +140 -0
- package/out/SixelHandler.js.map +1 -0
- package/package.json +31 -0
- package/src/IIPHandler.ts +161 -0
- package/src/IIPHeaderParser.test.ts +138 -0
- package/src/IIPHeaderParser.ts +186 -0
- package/src/IIPMetrics.test.ts +43 -0
- package/src/IIPMetrics.ts +81 -0
- package/src/ImageAddon.ts +317 -0
- package/src/ImageRenderer.ts +379 -0
- package/src/ImageStorage.ts +592 -0
- package/src/SixelHandler.ts +151 -0
- package/src/Types.d.ts +108 -0
- package/typings/addon-image.d.ts +120 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SixelHandler = void 0;
|
|
8
|
+
const Colors_1 = require("sixel/lib/Colors");
|
|
9
|
+
const ImageRenderer_1 = require("./ImageRenderer");
|
|
10
|
+
const Decoder_1 = require("sixel/lib/Decoder");
|
|
11
|
+
// always free decoder ressources after decoding if it exceeds this limit
|
|
12
|
+
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
|
13
|
+
// custom default palette: VT340 (lower 16 colors) + ANSI256 (up to 256) + zeroed (up to 4096)
|
|
14
|
+
const DEFAULT_PALETTE = Colors_1.PALETTE_ANSI_256;
|
|
15
|
+
DEFAULT_PALETTE.set(Colors_1.PALETTE_VT340_COLOR);
|
|
16
|
+
class SixelHandler {
|
|
17
|
+
constructor(_opts, _storage, _coreTerminal) {
|
|
18
|
+
this._opts = _opts;
|
|
19
|
+
this._storage = _storage;
|
|
20
|
+
this._coreTerminal = _coreTerminal;
|
|
21
|
+
this._size = 0;
|
|
22
|
+
this._aborted = false;
|
|
23
|
+
(0, Decoder_1.DecoderAsync)({
|
|
24
|
+
memoryLimit: this._opts.pixelLimit * 4,
|
|
25
|
+
palette: DEFAULT_PALETTE,
|
|
26
|
+
paletteLimit: this._opts.sixelPaletteLimit
|
|
27
|
+
}).then(d => this._dec = d);
|
|
28
|
+
}
|
|
29
|
+
reset() {
|
|
30
|
+
/**
|
|
31
|
+
* reset sixel decoder to defaults:
|
|
32
|
+
* - release all memory
|
|
33
|
+
* - nullify palette (4096)
|
|
34
|
+
* - apply default palette (256)
|
|
35
|
+
*/
|
|
36
|
+
if (this._dec) {
|
|
37
|
+
this._dec.release();
|
|
38
|
+
// FIXME: missing interface on decoder to nullify full palette
|
|
39
|
+
this._dec._palette.fill(0);
|
|
40
|
+
this._dec.init(0, DEFAULT_PALETTE, this._opts.sixelPaletteLimit);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
hook(params) {
|
|
44
|
+
var _a;
|
|
45
|
+
this._size = 0;
|
|
46
|
+
this._aborted = false;
|
|
47
|
+
if (this._dec) {
|
|
48
|
+
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);
|
|
49
|
+
this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
put(data, start, end) {
|
|
53
|
+
if (this._aborted || !this._dec) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._size += end - start;
|
|
57
|
+
if (this._size > this._opts.sixelSizeLimit) {
|
|
58
|
+
console.warn(`SIXEL: too much data, aborting`);
|
|
59
|
+
this._aborted = true;
|
|
60
|
+
this._dec.release();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
this._dec.decode(data, start, end);
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.warn(`SIXEL: error while decoding image - ${e}`);
|
|
68
|
+
this._aborted = true;
|
|
69
|
+
this._dec.release();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
unhook(success) {
|
|
73
|
+
var _a;
|
|
74
|
+
if (this._aborted || !success || !this._dec) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const width = this._dec.width;
|
|
78
|
+
const height = this._dec.height;
|
|
79
|
+
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
|
|
80
|
+
if (!width || !height) {
|
|
81
|
+
if (height) {
|
|
82
|
+
this._storage.advanceCursor(height);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const canvas = ImageRenderer_1.ImageRenderer.createCanvas(undefined, width, height);
|
|
87
|
+
(_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
|
|
88
|
+
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
|
89
|
+
this._dec.release();
|
|
90
|
+
}
|
|
91
|
+
this._storage.addImage(canvas);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.SixelHandler = SixelHandler;
|
|
96
|
+
/**
|
|
97
|
+
* Some helpers to extract current terminal colors.
|
|
98
|
+
*/
|
|
99
|
+
// get currently active background color from terminal
|
|
100
|
+
// also respect INVERSE setting
|
|
101
|
+
function extractActiveBg(attr, colors) {
|
|
102
|
+
let bg = 0;
|
|
103
|
+
if (!colors) {
|
|
104
|
+
// FIXME: theme service is prolly not available yet,
|
|
105
|
+
// happens if .open() was not called yet (bug in core?)
|
|
106
|
+
return bg;
|
|
107
|
+
}
|
|
108
|
+
if (attr.isInverse()) {
|
|
109
|
+
if (attr.isFgDefault()) {
|
|
110
|
+
bg = convertLe(colors.foreground.rgba);
|
|
111
|
+
}
|
|
112
|
+
else if (attr.isFgRGB()) {
|
|
113
|
+
const t = attr.constructor.toColorRGB(attr.getFgColor());
|
|
114
|
+
bg = (0, Colors_1.toRGBA8888)(...t);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
bg = convertLe(colors.ansi[attr.getFgColor()].rgba);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (attr.isBgDefault()) {
|
|
122
|
+
bg = convertLe(colors.background.rgba);
|
|
123
|
+
}
|
|
124
|
+
else if (attr.isBgRGB()) {
|
|
125
|
+
const t = attr.constructor.toColorRGB(attr.getBgColor());
|
|
126
|
+
bg = (0, Colors_1.toRGBA8888)(...t);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
bg = convertLe(colors.ansi[attr.getBgColor()].rgba);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return bg;
|
|
133
|
+
}
|
|
134
|
+
// rgba values on the color managers are always in BE, thus convert to LE
|
|
135
|
+
function convertLe(color) {
|
|
136
|
+
if (Colors_1.BIG_ENDIAN)
|
|
137
|
+
return color;
|
|
138
|
+
return (color & 0xFF) << 24 | (color >>> 8 & 0xFF) << 16 | (color >>> 16 & 0xFF) << 8 | color >>> 24 & 0xFF;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=SixelHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SixelHandler.js","sourceRoot":"","sources":["../src/SixelHandler.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAIH,6CAAiG;AAEjG,mDAAgD;AAEhD,+CAA0D;AAE1D,yEAAyE;AACzE,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,+CAA+C;AAEhF,8FAA8F;AAC9F,MAAM,eAAe,GAAG,yBAAgB,CAAC;AACzC,eAAe,CAAC,GAAG,CAAC,4BAAmB,CAAC,CAAC;AAGzC,MAAa,YAAY;IAKvB,YACmB,KAAyB,EACzB,QAAsB,EACtB,aAA2B;QAF3B,UAAK,GAAL,KAAK,CAAoB;QACzB,aAAQ,GAAR,QAAQ,CAAc;QACtB,kBAAa,GAAb,aAAa,CAAc;QAPtC,UAAK,GAAG,CAAC,CAAC;QACV,aAAQ,GAAG,KAAK,CAAC;QAQvB,IAAA,sBAAY,EAAC;YACX,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC;YACtC,OAAO,EAAE,eAAe;YACxB,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB;SAC3C,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC9B,CAAC;IAEM,KAAK;QACV;;;;;WAKG;QACH,IAAI,IAAI,CAAC,IAAI,EAAE;YACb,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACpB,8DAA8D;YAC7D,IAAI,CAAC,IAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;SAClE;IACH,CAAC;IAEM,IAAI,CAAC,MAAe;;QACzB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,IAAI,CAAC,IAAI,EAAE;YACb,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAC5D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,YAAY,EACnD,MAAA,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,aAAa,0CAAE,MAAM,CAAC,CAAC;YAClD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;SAC/D;IACH,CAAC;IAEM,GAAG,CAAC,IAAiB,EAAE,KAAa,EAAE,GAAW;QACtD,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAC/B,OAAO;SACR;QACD,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC;QAC1B,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE;YAC1C,OAAO,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;YAC/C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO;SACR;QACD,IAAI;YACF,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;SACpC;QAAC,OAAO,CAAC,EAAE;YACV,OAAO,CAAC,IAAI,CAAC,uCAAuC,CAAC,EAAE,CAAC,CAAC;YACzD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;SACrB;IACH,CAAC;IAEM,MAAM,CAAC,OAAgB;;QAC5B,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAC3C,OAAO,IAAI,CAAC;SACb;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAEhC,uEAAuE;QACvE,IAAI,CAAC,KAAK,IAAI,CAAE,MAAM,EAAE;YACtB,IAAI,MAAM,EAAE;gBACV,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;aACrC;YACD,OAAO,IAAI,CAAC;SACb;QAED,MAAM,MAAM,GAAG,6BAAa,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACpE,MAAA,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,0CAAE,YAAY,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3F,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,eAAe,EAAE;YAC3C,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;SACrB;QACD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAvFD,oCAuFC;AAGD;;GAEG;AAEH,sDAAsD;AACtD,+BAA+B;AAC/B,SAAS,eAAe,CAAC,IAAmB,EAAE,MAAoC;IAChF,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,CAAC,MAAM,EAAE;QACX,oDAAoD;QACpD,uDAAuD;QACvD,OAAO,EAAE,CAAC;KACX;IACD,IAAI,IAAI,CAAC,SAAS,EAAE,EAAE;QACpB,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE;YACtB,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;SACxC;aAAM,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE;YACzB,MAAM,CAAC,GAAI,IAAI,CAAC,WAAoC,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACnF,EAAE,GAAG,IAAA,mBAAU,EAAC,GAAG,CAAC,CAAC,CAAC;SACvB;aAAM;YACL,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;SACrD;KACF;SAAM;QACL,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE;YACtB,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;SACxC;aAAM,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE;YACzB,MAAM,CAAC,GAAI,IAAI,CAAC,WAAoC,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACnF,EAAE,GAAG,IAAA,mBAAU,EAAC,GAAG,CAAC,CAAC,CAAC;SACvB;aAAM;YACL,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;SACrD;KACF;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,yEAAyE;AACzE,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,mBAAU;QAAE,OAAO,KAAK,CAAC;IAC7B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,EAAE,GAAG,IAAI,CAAC;AAC9G,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xterm/addon-image",
|
|
3
|
+
"version": "0.7.0-beta.1",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "The xterm.js authors",
|
|
6
|
+
"url": "https://xtermjs.org/"
|
|
7
|
+
},
|
|
8
|
+
"main": "lib/addon-image.js",
|
|
9
|
+
"types": "typings/addon-image.d.ts",
|
|
10
|
+
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"terminal",
|
|
14
|
+
"image",
|
|
15
|
+
"sixel",
|
|
16
|
+
"xterm",
|
|
17
|
+
"xterm.js"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"prepackage": "../../node_modules/.bin/tsc -p .",
|
|
21
|
+
"package": "../../node_modules/.bin/webpack",
|
|
22
|
+
"prepublishOnly": "npm run package"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@xterm/xterm": "^5.2.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"sixel": "^0.16.0",
|
|
29
|
+
"xterm-wasm-parts": "^0.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
|
6
|
+
import { ImageRenderer } from './ImageRenderer';
|
|
7
|
+
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
|
8
|
+
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
|
9
|
+
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
|
10
|
+
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line
|
|
14
|
+
declare const Buffer: any;
|
|
15
|
+
|
|
16
|
+
// limit hold memory in base64 decoder
|
|
17
|
+
const KEEP_DATA = 4194304;
|
|
18
|
+
|
|
19
|
+
// default IIP header values
|
|
20
|
+
const DEFAULT_HEADER: IHeaderFields = {
|
|
21
|
+
name: 'Unnamed file',
|
|
22
|
+
size: 0,
|
|
23
|
+
width: 'auto',
|
|
24
|
+
height: 'auto',
|
|
25
|
+
preserveAspectRatio: 1,
|
|
26
|
+
inline: 0
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export class IIPHandler implements IOscHandler, IResetHandler {
|
|
31
|
+
private _aborted = false;
|
|
32
|
+
private _hp = new HeaderParser();
|
|
33
|
+
private _header: IHeaderFields = DEFAULT_HEADER;
|
|
34
|
+
private _dec = new Base64Decoder(KEEP_DATA);
|
|
35
|
+
private _metrics = UNSUPPORTED_TYPE;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly _opts: IImageAddonOptions,
|
|
39
|
+
private readonly _renderer: ImageRenderer,
|
|
40
|
+
private readonly _storage: ImageStorage,
|
|
41
|
+
private readonly _coreTerminal: ITerminalExt
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
public reset(): void {}
|
|
45
|
+
|
|
46
|
+
public start(): void {
|
|
47
|
+
this._aborted = false;
|
|
48
|
+
this._header = DEFAULT_HEADER;
|
|
49
|
+
this._metrics = UNSUPPORTED_TYPE;
|
|
50
|
+
this._hp.reset();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public put(data: Uint32Array, start: number, end: number): void {
|
|
54
|
+
if (this._aborted) return;
|
|
55
|
+
|
|
56
|
+
if (this._hp.state === HeaderState.END) {
|
|
57
|
+
if (this._dec.put(data, start, end)) {
|
|
58
|
+
this._dec.release();
|
|
59
|
+
this._aborted = true;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const dataPos = this._hp.parse(data, start, end);
|
|
63
|
+
if (dataPos === -1) {
|
|
64
|
+
this._aborted = true;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (dataPos > 0) {
|
|
68
|
+
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
|
69
|
+
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
|
|
70
|
+
this._aborted = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this._dec.init(this._header.size);
|
|
74
|
+
if (this._dec.put(data, dataPos, end)) {
|
|
75
|
+
this._dec.release();
|
|
76
|
+
this._aborted = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public end(success: boolean): boolean | Promise<boolean> {
|
|
83
|
+
if (this._aborted) return true;
|
|
84
|
+
|
|
85
|
+
let w = 0;
|
|
86
|
+
let h = 0;
|
|
87
|
+
|
|
88
|
+
// early exit condition chain
|
|
89
|
+
let cond: number | boolean = true;
|
|
90
|
+
if (cond = success) {
|
|
91
|
+
if (cond = !this._dec.end()) {
|
|
92
|
+
this._metrics = imageType(this._dec.data8);
|
|
93
|
+
if (cond = this._metrics.mime !== 'unsupported') {
|
|
94
|
+
w = this._metrics.width;
|
|
95
|
+
h = this._metrics.height;
|
|
96
|
+
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
|
97
|
+
[w, h] = this._resize(w, h).map(Math.floor);
|
|
98
|
+
cond = w && h && w * h < this._opts.pixelLimit;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!cond) {
|
|
104
|
+
this._dec.release();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
|
109
|
+
this._dec.release();
|
|
110
|
+
|
|
111
|
+
if (!window.createImageBitmap) {
|
|
112
|
+
const url = URL.createObjectURL(blob);
|
|
113
|
+
const img = new Image();
|
|
114
|
+
return new Promise<boolean>(r => {
|
|
115
|
+
img.addEventListener('load', () => {
|
|
116
|
+
URL.revokeObjectURL(url);
|
|
117
|
+
const canvas = ImageRenderer.createCanvas(window.document, w, h);
|
|
118
|
+
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
|
|
119
|
+
this._storage.addImage(canvas);
|
|
120
|
+
r(true);
|
|
121
|
+
});
|
|
122
|
+
img.src = url;
|
|
123
|
+
// sanity measure to avoid terminal blocking from dangling promise
|
|
124
|
+
// happens from corrupt data (onload never gets fired)
|
|
125
|
+
setTimeout(() => r(true), 1000);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
|
129
|
+
.then(bm => {
|
|
130
|
+
this._storage.addImage(bm);
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private _resize(w: number, h: number): [number, number] {
|
|
136
|
+
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
|
137
|
+
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
|
138
|
+
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
|
|
139
|
+
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
|
|
140
|
+
|
|
141
|
+
const rw = this._dim(this._header.width!, width, cw);
|
|
142
|
+
const rh = this._dim(this._header.height!, height, ch);
|
|
143
|
+
if (!rw && !rh) {
|
|
144
|
+
const wf = width / w; // TODO: should this respect initial cursor offset?
|
|
145
|
+
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
|
146
|
+
const f = Math.min(wf, hf);
|
|
147
|
+
return f < 1 ? [w * f, h * f] : [w, h];
|
|
148
|
+
}
|
|
149
|
+
return !rw
|
|
150
|
+
? [w * rh / h, rh]
|
|
151
|
+
: this._header.preserveAspectRatio || !rw || !rh
|
|
152
|
+
? [rw, h * rw / w] : [rw, rh];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private _dim(s: string, total: number, cdim: number): number {
|
|
156
|
+
if (s === 'auto') return 0;
|
|
157
|
+
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
|
|
158
|
+
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
|
|
159
|
+
return parseInt(s) * cdim;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { assert } from 'chai';
|
|
7
|
+
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const CASES: [string, IHeaderFields][] = [
|
|
11
|
+
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
|
|
12
|
+
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
|
|
13
|
+
// utf-8 encoding in name
|
|
14
|
+
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
|
|
15
|
+
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
|
|
16
|
+
// full header spec
|
|
17
|
+
[
|
|
18
|
+
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
|
19
|
+
{
|
|
20
|
+
inline: 1,
|
|
21
|
+
width: '10px',
|
|
22
|
+
height: '20%',
|
|
23
|
+
preserveAspectRatio: 1,
|
|
24
|
+
size: 123456,
|
|
25
|
+
name: 'ümläutß'
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
|
30
|
+
{
|
|
31
|
+
inline: 1,
|
|
32
|
+
width: 'auto',
|
|
33
|
+
height: '20',
|
|
34
|
+
preserveAspectRatio: 1,
|
|
35
|
+
size: 123456,
|
|
36
|
+
name: 'ümläutß'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function fromBs(bs: string): Uint32Array {
|
|
42
|
+
const r = new Uint32Array(bs.length);
|
|
43
|
+
for (let i = 0; i < r.length; ++i) r[i] = bs.charCodeAt(i);
|
|
44
|
+
return r;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('IIPHeaderParser', () => {
|
|
48
|
+
it('at once', () => {
|
|
49
|
+
const hp = new HeaderParser();
|
|
50
|
+
for (const example of CASES) {
|
|
51
|
+
hp.reset();
|
|
52
|
+
const inp = fromBs(example[0]);
|
|
53
|
+
const res = hp.parse(inp, 0, inp.length);
|
|
54
|
+
assert.strictEqual(res, inp.length);
|
|
55
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
56
|
+
assert.deepEqual(hp.fields, example[1]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
it('bytewise', () => {
|
|
60
|
+
const hp = new HeaderParser();
|
|
61
|
+
for (const example of CASES) {
|
|
62
|
+
hp.reset();
|
|
63
|
+
const inp = fromBs(example[0]);
|
|
64
|
+
let pos = 0;
|
|
65
|
+
let res = -2;
|
|
66
|
+
while (res === -2 && pos < inp.length) {
|
|
67
|
+
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
|
68
|
+
}
|
|
69
|
+
assert.strictEqual(res, 1);
|
|
70
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
71
|
+
assert.deepEqual(hp.fields, example[1]);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
it('no File= starter', () => {
|
|
75
|
+
const hp = new HeaderParser();
|
|
76
|
+
let inp = fromBs('size=123456;name=dGVzdA==:');
|
|
77
|
+
let res = hp.parse(inp, 0, inp.length);
|
|
78
|
+
assert.strictEqual(res, -1);
|
|
79
|
+
hp.reset();
|
|
80
|
+
inp = fromBs(CASES[0][0]);
|
|
81
|
+
res = hp.parse(inp, 0, inp.length);
|
|
82
|
+
assert.strictEqual(res, inp.length);
|
|
83
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
84
|
+
assert.deepEqual(hp.fields, CASES[0][1]);
|
|
85
|
+
});
|
|
86
|
+
it('empty key - error', () => {
|
|
87
|
+
const hp = new HeaderParser();
|
|
88
|
+
let inp = fromBs('File=size=123456;=dGVzdA==:');
|
|
89
|
+
let res = hp.parse(inp, 0, inp.length);
|
|
90
|
+
assert.strictEqual(res, -1);
|
|
91
|
+
hp.reset();
|
|
92
|
+
inp = fromBs(CASES[0][0]);
|
|
93
|
+
res = hp.parse(inp, 0, inp.length);
|
|
94
|
+
assert.strictEqual(res, inp.length);
|
|
95
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
96
|
+
assert.deepEqual(hp.fields, CASES[0][1]);
|
|
97
|
+
});
|
|
98
|
+
it('empty size value - set to 0', () => {
|
|
99
|
+
const hp = new HeaderParser();
|
|
100
|
+
let inp = fromBs('File=size=;name=dGVzdA==:');
|
|
101
|
+
let res = hp.parse(inp, 0, inp.length);
|
|
102
|
+
assert.strictEqual(res, inp.length);
|
|
103
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
104
|
+
assert.deepEqual(hp.fields, {name: 'test', size: 0});
|
|
105
|
+
hp.reset();
|
|
106
|
+
inp = fromBs(CASES[0][0]);
|
|
107
|
+
res = hp.parse(inp, 0, inp.length);
|
|
108
|
+
assert.strictEqual(res, inp.length);
|
|
109
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
110
|
+
assert.deepEqual(hp.fields, CASES[0][1]);
|
|
111
|
+
});
|
|
112
|
+
it('empty name value - set to empty string', () => {
|
|
113
|
+
const hp = new HeaderParser();
|
|
114
|
+
let inp = fromBs('File=size=123456;name=:');
|
|
115
|
+
let res = hp.parse(inp, 0, inp.length);
|
|
116
|
+
assert.strictEqual(res, inp.length);
|
|
117
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
118
|
+
assert.deepEqual(hp.fields, {name: '', size: 123456});
|
|
119
|
+
hp.reset();
|
|
120
|
+
inp = fromBs(CASES[0][0]);
|
|
121
|
+
res = hp.parse(inp, 0, inp.length);
|
|
122
|
+
assert.strictEqual(res, inp.length);
|
|
123
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
124
|
+
assert.deepEqual(hp.fields, CASES[0][1]);
|
|
125
|
+
});
|
|
126
|
+
it('empty size value - error', () => {
|
|
127
|
+
const hp = new HeaderParser();
|
|
128
|
+
let inp = fromBs('File=inline=1;width=;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:');
|
|
129
|
+
let res = hp.parse(inp, 0, inp.length);
|
|
130
|
+
assert.strictEqual(res, -1);
|
|
131
|
+
hp.reset();
|
|
132
|
+
inp = fromBs(CASES[0][0]);
|
|
133
|
+
res = hp.parse(inp, 0, inp.length);
|
|
134
|
+
assert.strictEqual(res, inp.length);
|
|
135
|
+
assert.strictEqual(hp.state, HeaderState.END);
|
|
136
|
+
assert.deepEqual(hp.fields, CASES[0][1]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line
|
|
7
|
+
declare const Buffer: any;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export interface IHeaderFields {
|
|
11
|
+
// base-64 encoded filename. Defaults to "Unnamed file".
|
|
12
|
+
name: string;
|
|
13
|
+
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
|
14
|
+
size: number;
|
|
15
|
+
/**
|
|
16
|
+
* Optional width and height to render:
|
|
17
|
+
* - N: N character cells.
|
|
18
|
+
* - Npx: N pixels.
|
|
19
|
+
* - N%: N percent of the session's width or height.
|
|
20
|
+
* - auto: The image's inherent size will be used to determine an appropriate dimension.
|
|
21
|
+
*/
|
|
22
|
+
width?: string;
|
|
23
|
+
height?: string;
|
|
24
|
+
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
|
|
25
|
+
preserveAspectRatio?: number;
|
|
26
|
+
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
|
|
27
|
+
// (download not supported).
|
|
28
|
+
inline?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const enum HeaderState {
|
|
32
|
+
START = 0,
|
|
33
|
+
ABORT = 1,
|
|
34
|
+
KEY = 2,
|
|
35
|
+
VALUE = 3,
|
|
36
|
+
END = 4
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// field value decoders
|
|
40
|
+
|
|
41
|
+
// ASCII bytes to string
|
|
42
|
+
function toStr(data: Uint32Array): string {
|
|
43
|
+
let s = '';
|
|
44
|
+
for (let i = 0; i < data.length; ++i) {
|
|
45
|
+
s += String.fromCharCode(data[i]);
|
|
46
|
+
}
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// digits to integer
|
|
51
|
+
function toInt(data: Uint32Array): number {
|
|
52
|
+
let v = 0;
|
|
53
|
+
for (let i = 0; i < data.length; ++i) {
|
|
54
|
+
if (data[i] < 48 || data[i] > 57) {
|
|
55
|
+
throw new Error('illegal char');
|
|
56
|
+
}
|
|
57
|
+
v = v * 10 + data[i] - 48;
|
|
58
|
+
}
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// check for correct size entry
|
|
63
|
+
function toSize(data: Uint32Array): string {
|
|
64
|
+
const v = toStr(data);
|
|
65
|
+
if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
|
|
66
|
+
throw new Error('illegal size');
|
|
67
|
+
}
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// name is base64 encoded utf-8
|
|
72
|
+
function toName(data: Uint32Array): string {
|
|
73
|
+
if (typeof Buffer !== 'undefined') {
|
|
74
|
+
return Buffer.from(toStr(data), 'base64').toString();
|
|
75
|
+
}
|
|
76
|
+
const bs = atob(toStr(data));
|
|
77
|
+
const b = new Uint8Array(bs.length);
|
|
78
|
+
for (let i = 0; i < b.length; ++i) {
|
|
79
|
+
b[i] = bs.charCodeAt(i);
|
|
80
|
+
}
|
|
81
|
+
return new TextDecoder().decode(b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
|
85
|
+
inline: toInt,
|
|
86
|
+
size: toInt,
|
|
87
|
+
name: toName,
|
|
88
|
+
width: toSize,
|
|
89
|
+
height: toSize,
|
|
90
|
+
preserveAspectRatio: toInt
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
const FILE_MARKER = [70, 105, 108, 101];
|
|
95
|
+
const MAX_FIELDCHARS = 1024;
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
export class HeaderParser {
|
|
99
|
+
public state: HeaderState = HeaderState.START;
|
|
100
|
+
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
|
101
|
+
private _position = 0;
|
|
102
|
+
private _key = '';
|
|
103
|
+
public fields: {[key: string]: any} = {};
|
|
104
|
+
|
|
105
|
+
public reset(): void {
|
|
106
|
+
this._buffer.fill(0);
|
|
107
|
+
this.state = HeaderState.START;
|
|
108
|
+
this._position = 0;
|
|
109
|
+
this.fields = {};
|
|
110
|
+
this._key = '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public parse(data: Uint32Array, start: number, end: number): number {
|
|
114
|
+
let state = this.state;
|
|
115
|
+
let pos = this._position;
|
|
116
|
+
const buffer = this._buffer;
|
|
117
|
+
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
|
|
118
|
+
if (state === HeaderState.START && pos > 6) return -1;
|
|
119
|
+
for (let i = start; i < end; ++i) {
|
|
120
|
+
const c = data[i];
|
|
121
|
+
switch (c) {
|
|
122
|
+
case 59: // ;
|
|
123
|
+
if (!this._storeValue(pos)) return this._a();
|
|
124
|
+
state = HeaderState.KEY;
|
|
125
|
+
pos = 0;
|
|
126
|
+
break;
|
|
127
|
+
case 61: // =
|
|
128
|
+
if (state === HeaderState.START) {
|
|
129
|
+
for (let k = 0; k < FILE_MARKER.length; ++k) {
|
|
130
|
+
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
|
131
|
+
}
|
|
132
|
+
state = HeaderState.KEY;
|
|
133
|
+
pos = 0;
|
|
134
|
+
} else if (state === HeaderState.KEY) {
|
|
135
|
+
if (!this._storeKey(pos)) return this._a();
|
|
136
|
+
state = HeaderState.VALUE;
|
|
137
|
+
pos = 0;
|
|
138
|
+
} else if (state === HeaderState.VALUE) {
|
|
139
|
+
if (pos >= MAX_FIELDCHARS) return this._a();
|
|
140
|
+
buffer[pos++] = c;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case 58: // :
|
|
144
|
+
if (state === HeaderState.VALUE) {
|
|
145
|
+
if (!this._storeValue(pos)) return this._a();
|
|
146
|
+
}
|
|
147
|
+
this.state = HeaderState.END;
|
|
148
|
+
return i + 1;
|
|
149
|
+
default:
|
|
150
|
+
if (pos >= MAX_FIELDCHARS) return this._a();
|
|
151
|
+
buffer[pos++] = c;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this.state = state;
|
|
155
|
+
this._position = pos;
|
|
156
|
+
return -2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _a(): number {
|
|
160
|
+
this.state = HeaderState.ABORT;
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _storeKey(pos: number): boolean {
|
|
165
|
+
const k = toStr(this._buffer.subarray(0, pos));
|
|
166
|
+
if (k) {
|
|
167
|
+
this._key = k;
|
|
168
|
+
this.fields[k] = null;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _storeValue(pos: number): boolean {
|
|
175
|
+
if (this._key) {
|
|
176
|
+
try {
|
|
177
|
+
const v = this._buffer.slice(0, pos);
|
|
178
|
+
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|