@sailfish-ai/addon-image 0.9.0

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.
@@ -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([new Uint8Array(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,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 {
180
+ return false;
181
+ }
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+
7
+ export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
8
+
9
+ export interface IMetrics {
10
+ mime: ImageType;
11
+ width: number;
12
+ height: number;
13
+ }
14
+
15
+ export const UNSUPPORTED_TYPE: IMetrics = {
16
+ mime: 'unsupported',
17
+ width: 0,
18
+ height: 0
19
+ };
20
+
21
+ export function imageType(d: Uint8Array): IMetrics {
22
+ if (d.length < 24) {
23
+ return UNSUPPORTED_TYPE;
24
+ }
25
+ const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
26
+ // PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
27
+ // + first chunk must be IHDR
28
+ if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
29
+ return {
30
+ mime: 'image/png',
31
+ width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
32
+ height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
33
+ };
34
+ }
35
+ // JPEG: FF D8 FF
36
+ if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
37
+ const [width, height] = jpgSize(d);
38
+ return { mime: 'image/jpeg', width, height };
39
+ }
40
+ // GIF: GIF87a or GIF89a
41
+ if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
42
+ return {
43
+ mime: 'image/gif',
44
+ width: d[7] << 8 | d[6],
45
+ height: d[9] << 8 | d[8]
46
+ };
47
+ }
48
+ return UNSUPPORTED_TYPE;
49
+ }
50
+
51
+ function jpgSize(d: Uint8Array): [number, number] {
52
+ const len = d.length;
53
+ let i = 4;
54
+ let blockLength = d[i] << 8 | d[i + 1];
55
+ while (true) {
56
+ i += blockLength;
57
+ if (i >= len) {
58
+ // exhausted without size info
59
+ return [0, 0];
60
+ }
61
+ if (d[i] !== 0xFF) {
62
+ return [0, 0];
63
+ }
64
+ if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
65
+ if (i + 8 < len) {
66
+ return [
67
+ d[i + 7] << 8 | d[i + 8],
68
+ d[i + 5] << 8 | d[i + 6]
69
+ ];
70
+ }
71
+ return [0, 0];
72
+ }
73
+ i += 2;
74
+ blockLength = d[i] << 8 | d[i + 1];
75
+ }
76
+ }