@xterm/addon-image 0.10.0-beta.31 → 0.10.0-beta.310

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xterm/addon-image",
3
- "version": "0.10.0-beta.31",
3
+ "version": "0.10.0-beta.310",
4
4
  "author": {
5
5
  "name": "The xterm.js authors",
6
6
  "url": "https://xtermjs.org/"
@@ -18,17 +18,17 @@
18
18
  "xterm.js"
19
19
  ],
20
20
  "scripts": {
21
- "prepackage": "../../node_modules/.bin/tsc -p .",
21
+ "prepackage": "../../node_modules/.bin/tsgo -p .",
22
22
  "package": "../../node_modules/.bin/webpack",
23
23
  "prepublishOnly": "npm run package",
24
24
  "start": "node ../../demo/start"
25
25
  },
26
26
  "devDependencies": {
27
27
  "sixel": "^0.16.0",
28
- "xterm-wasm-parts": "^0.1.0"
28
+ "xterm-wasm-parts": "^0.4.1"
29
29
  },
30
- "commit": "df8f9fc5199bec2273ce48f5e4a4349b65283d67",
30
+ "commit": "991857a1b1cacf06f9531ba74a6e927a7a6398bb",
31
31
  "peerDependencies": {
32
- "@xterm/xterm": "^6.1.0-beta.31"
32
+ "@xterm/xterm": "^6.1.0-beta.315"
33
33
  }
34
34
  }
package/src/IIPHandler.ts CHANGED
@@ -4,20 +4,26 @@
4
4
  */
5
5
  import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
6
6
  import { ImageRenderer } from './ImageRenderer';
7
- import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
7
+ import { IIPImageStorage } from './IIPImageStorage';
8
+ import { CELL_SIZE_DEFAULT } from './ImageStorage';
8
9
  import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
9
- import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
10
+ import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
11
+ import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
10
12
  import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
11
13
 
12
-
13
- // eslint-disable-next-line
14
- declare const Buffer: any;
15
-
16
- // limit hold memory in base64 decoder
17
- const KEEP_DATA = 4194304;
14
+ // Local const enum mirror - esbuild can't inline const enums from external packages
15
+ const enum DecoderConst {
16
+ // Limit held memory in base64 decoder (encoded bytes).
17
+ KEEP_DATA = 4194304,
18
+ // Initial buffer allocation for the decoder.
19
+ INITIAL_DATA = 1048576,
20
+ // Local mirror of const enum (esbuild can't inline const enums from external packages)
21
+ OK = 0
22
+ }
18
23
 
19
24
  // default IIP header values
20
25
  const DEFAULT_HEADER: IHeaderFields = {
26
+ type: SequenceType.INVALID,
21
27
  name: 'Unnamed file',
22
28
  size: 0,
23
29
  width: 'auto',
@@ -31,22 +37,31 @@ export class IIPHandler implements IOscHandler, IResetHandler {
31
37
  private _aborted = false;
32
38
  private _hp = new HeaderParser();
33
39
  private _header: IHeaderFields = DEFAULT_HEADER;
34
- private _dec = new Base64Decoder(KEEP_DATA);
35
- private _metrics = UNSUPPORTED_TYPE;
40
+ private _dec: Base64Decoder;
41
+ private _qoiDec: QoiDecoder;
42
+ private _isMultipart = false;
43
+ private _abortMulti = false;
36
44
 
37
45
  constructor(
38
46
  private readonly _opts: IImageAddonOptions,
39
47
  private readonly _renderer: ImageRenderer,
40
- private readonly _storage: ImageStorage,
48
+ private readonly _storage: IIPImageStorage,
41
49
  private readonly _coreTerminal: ITerminalExt
42
- ) {}
50
+ ) {
51
+ const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
52
+ const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
53
+ this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
54
+ this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
55
+ }
43
56
 
44
- public reset(): void {}
57
+ public reset(): void {
58
+ this._hp.reset();
59
+ this._dec.release();
60
+ this._qoiDec.release();
61
+ }
45
62
 
46
63
  public start(): void {
47
64
  this._aborted = false;
48
- this._header = DEFAULT_HEADER;
49
- this._metrics = UNSUPPORTED_TYPE;
50
65
  this._hp.reset();
51
66
  }
52
67
 
@@ -54,7 +69,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
54
69
  if (this._aborted) return;
55
70
 
56
71
  if (this._hp.state === HeaderState.END) {
57
- if (this._dec.put(data, start, end)) {
72
+ if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
58
73
  this._dec.release();
59
74
  this._aborted = true;
60
75
  }
@@ -65,15 +80,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
65
80
  return;
66
81
  }
67
82
  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) {
83
+ const seqType = this._hp.fields.type;
84
+ if (seqType === SequenceType.FILE) {
85
+ if (this._isMultipart) {
86
+ this._isMultipart = false;
87
+ this._abortMulti = false;
88
+ this._dec.release();
89
+ }
90
+ this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
91
+ if (!this._header.inline) {
92
+ this._aborted = true;
93
+ return;
94
+ }
95
+ this._dec.init();
96
+ } else if (this._abortMulti) {
70
97
  this._aborted = true;
71
98
  return;
72
99
  }
73
- this._dec.init(this._header.size);
74
- if (this._dec.put(data, dataPos, end)) {
100
+ if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
75
101
  this._dec.release();
76
102
  this._aborted = true;
103
+ if (this._isMultipart) this._abortMulti = true;
77
104
  }
78
105
  }
79
106
  }
@@ -82,22 +109,67 @@ export class IIPHandler implements IOscHandler, IResetHandler {
82
109
  public end(success: boolean): boolean | Promise<boolean> {
83
110
  if (this._aborted) return true;
84
111
 
112
+ if (this._hp.state !== HeaderState.END) {
113
+ if (this._hp.end()) return true;
114
+ }
115
+ const seqType = this._hp.fields.type;
116
+
117
+ if (seqType === SequenceType.FILEPART) return true;
118
+
119
+ if (seqType === SequenceType.REPORTCELLSIZE) {
120
+ // OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
121
+ let w = CELL_SIZE_DEFAULT.width;
122
+ let h = CELL_SIZE_DEFAULT.height;
123
+ if (this._renderer.dimensions) {
124
+ w = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
125
+ h = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
126
+ }
127
+ const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
128
+ const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
129
+ this._coreTerminal.input(report, false);
130
+ return true;
131
+ }
132
+
133
+ if (seqType === SequenceType.MULTIPARTFILE) {
134
+ this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
135
+ this._isMultipart = true;
136
+ this._abortMulti = false;
137
+ this._dec.release();
138
+ this._dec.init();
139
+ return true;
140
+ }
141
+
142
+ if (seqType === SequenceType.FILEEND) {
143
+ if (!this._isMultipart) return true;
144
+ this._isMultipart = false;
145
+ if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
146
+ }
147
+
148
+ // fallthrough for SequenceType.FILE & SequenceType.FILEEND
149
+
85
150
  let w = 0;
86
151
  let h = 0;
87
152
 
88
153
  // early exit condition chain
89
- let cond: number | boolean = true;
154
+ let cond: number | boolean;
155
+ let metrics = UNSUPPORTED_TYPE;
90
156
  if (cond = success) {
91
157
  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;
158
+ metrics = imageType(this._dec.data8);
159
+ if (cond = metrics.mime !== 'unsupported') {
160
+ w = metrics.width;
161
+ h = metrics.height;
96
162
  if (cond = w && h && w * h < this._opts.pixelLimit) {
97
163
  [w, h] = this._resize(w, h).map(Math.floor);
98
164
  cond = w && h && w * h < this._opts.pixelLimit;
165
+ } else {
166
+ console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
99
167
  }
168
+ } else {
169
+ console.warn('IIP: unsupported image type');
100
170
  }
171
+ } else {
172
+ console.warn('IIP: error during BASE64 decoding');
101
173
  }
102
174
  }
103
175
  if (!cond) {
@@ -105,30 +177,35 @@ export class IIPHandler implements IOscHandler, IResetHandler {
105
177
  return true;
106
178
  }
107
179
 
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
- });
180
+ let blob: Blob | ImageData;
181
+ if (metrics.mime === 'image/qoi') {
182
+ const data = this._qoiDec.decode(this._dec.data8);
183
+ blob = new ImageData(
184
+ new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
185
+ this._qoiDec.width,
186
+ this._qoiDec.height
187
+ );
188
+ this._qoiDec.release();
189
+ if (w === this._qoiDec.width && h === this._qoiDec.height) {
190
+ // use fast-path if we don't need to rescale
191
+ this._dec.release();
192
+ const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
193
+ canvas.getContext('2d')?.putImageData(blob, 0, 0);
194
+ this._storage.addImage(canvas);
195
+ return true;
196
+ }
197
+ } else {
198
+ blob = new Blob([this._dec.data8], { type: metrics.mime });
127
199
  }
200
+ this._dec.release();
128
201
  return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
129
202
  .then(bm => {
130
203
  this._storage.addImage(bm);
131
204
  return true;
205
+ })
206
+ .catch(e => {
207
+ console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
208
+ return true;
132
209
  });
133
210
  }
134
211
 
@@ -154,8 +231,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
154
231
 
155
232
  private _dim(s: string, total: number, cdim: number): number {
156
233
  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;
234
+ if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
235
+ if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
236
+ return parseInt(s, 10) * cdim;
160
237
  }
161
238
  }
@@ -6,8 +6,27 @@
6
6
  // eslint-disable-next-line
7
7
  declare const Buffer: any;
8
8
 
9
+ export const enum HeaderState {
10
+ START = 0,
11
+ ABORT = 1,
12
+ KEY = 2,
13
+ VALUE = 3,
14
+ END = 4
15
+ }
16
+
17
+ export const enum SequenceType {
18
+ INVALID = 0,
19
+ FILE = 1,
20
+ MULTIPARTFILE = 2,
21
+ FILEPART = 3,
22
+ FILEEND = 4,
23
+ REPORTCELLSIZE = 5
24
+ }
9
25
 
10
26
  export interface IHeaderFields {
27
+ [key: string]: number | string | Uint32Array | null | undefined;
28
+ // sequence type
29
+ type: SequenceType;
11
30
  // base-64 encoded filename. Defaults to "Unnamed file".
12
31
  name: string;
13
32
  // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -28,14 +47,6 @@ export interface IHeaderFields {
28
47
  inline?: number;
29
48
  }
30
49
 
31
- export const enum HeaderState {
32
- START = 0,
33
- ABORT = 1,
34
- KEY = 2,
35
- VALUE = 3,
36
- END = 4
37
- }
38
-
39
50
  // field value decoders
40
51
 
41
52
  // ASCII bytes to string
@@ -81,7 +92,7 @@ function toName(data: Uint32Array): string {
81
92
  return new TextDecoder().decode(b);
82
93
  }
83
94
 
84
- const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
95
+ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
85
96
  inline: toInt,
86
97
  size: toInt,
87
98
  name: toName,
@@ -91,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
91
102
  };
92
103
 
93
104
 
105
+ // sequence type markers
106
+ // File
94
107
  const FILE_MARKER = [70, 105, 108, 101];
108
+ // MultipartFile
109
+ const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
110
+ // FilePart
111
+ const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
112
+ // FileEnd
113
+ const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
114
+ // ReportCellSize
115
+ const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
116
+
117
+ // max allowed chars for sequence header
95
118
  const MAX_FIELDCHARS = 1024;
96
119
 
97
120
 
@@ -100,7 +123,7 @@ export class HeaderParser {
100
123
  private _buffer = new Uint32Array(MAX_FIELDCHARS);
101
124
  private _position = 0;
102
125
  private _key = '';
103
- public fields: {[key: string]: any} = {};
126
+ public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
104
127
 
105
128
  public reset(): void {
106
129
  this._buffer.fill(0);
@@ -110,12 +133,43 @@ export class HeaderParser {
110
133
  this._key = '';
111
134
  }
112
135
 
136
+ public end(): number {
137
+ if (this.state === HeaderState.START) {
138
+ if (this._position === FILEEND_MARKER.length) {
139
+ for (let k = 0; k < FILEEND_MARKER.length; ++k) {
140
+ if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
141
+ }
142
+ this.fields['type'] = SequenceType.FILEEND;
143
+ this.state = HeaderState.END;
144
+ return 0;
145
+ }
146
+ if (this._position === REPORTCELLSIZE_MARKER.length) {
147
+ for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
148
+ if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
149
+ }
150
+ this.fields['type'] = SequenceType.REPORTCELLSIZE;
151
+ this.state = HeaderState.END;
152
+ return 0;
153
+ }
154
+ return this._a();
155
+ }
156
+ if (this.state === HeaderState.END) return 0;
157
+ if (this.state === HeaderState.VALUE
158
+ && this.fields.type === SequenceType.MULTIPARTFILE
159
+ ) {
160
+ if (!this._storeValue(this._position)) return this._a();
161
+ this.state = HeaderState.END;
162
+ return 0;
163
+ }
164
+ return this._a();
165
+ }
166
+
113
167
  public parse(data: Uint32Array, start: number, end: number): number {
114
168
  let state = this.state;
115
169
  let pos = this._position;
116
170
  const buffer = this._buffer;
117
171
  if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
118
- if (state === HeaderState.START && pos > 6) return -1;
172
+ if (state === HeaderState.START && pos > 14) return -1;
119
173
  for (let i = start; i < end; ++i) {
120
174
  const c = data[i];
121
175
  switch (c) {
@@ -126,8 +180,29 @@ export class HeaderParser {
126
180
  break;
127
181
  case 61: // =
128
182
  if (state === HeaderState.START) {
129
- for (let k = 0; k < FILE_MARKER.length; ++k) {
130
- if (buffer[k] !== FILE_MARKER[k]) return this._a();
183
+ if (buffer[0] === 70) {
184
+ // 'File' or 'FilePart'
185
+ let k = 0;
186
+ for (; k < FILE_MARKER.length; ++k) {
187
+ if (buffer[k] !== FILE_MARKER[k]) return this._a();
188
+ }
189
+ this.fields['type'] = SequenceType.FILE;
190
+ if (pos === FILEPART_MARKER.length) {
191
+ for (; k < FILEPART_MARKER.length; ++k) {
192
+ if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
193
+ }
194
+ this.fields['type'] = SequenceType.FILEPART;
195
+ this.state = HeaderState.END;
196
+ return i + 1;
197
+ }
198
+ } else if (buffer[0] === 77) {
199
+ // 'MultipartFile'
200
+ for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
201
+ if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
202
+ }
203
+ this.fields['type'] = SequenceType.MULTIPARTFILE;
204
+ } else {
205
+ return this._a();
131
206
  }
132
207
  state = HeaderState.KEY;
133
208
  pos = 0;
@@ -157,6 +232,7 @@ export class HeaderParser {
157
232
  }
158
233
 
159
234
  private _a(): number {
235
+ this.fields.type = SequenceType.INVALID;
160
236
  this.state = HeaderState.ABORT;
161
237
  return -1;
162
238
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { IAddImageOpts } from './Types';
7
+ import { ImageStorage } from './ImageStorage';
8
+
9
+ /**
10
+ * IIP (iTerm Image Protocol) specific image storage controller.
11
+ *
12
+ * Wraps the shared ImageStorage with IIP protocol semantics:
13
+ * - Always uses scrolling mode (cursor advances with image)
14
+ */
15
+ export class IIPImageStorage {
16
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
17
+ constructor(
18
+ private readonly _storage: ImageStorage
19
+ ) {}
20
+
21
+ /**
22
+ * Add an IIP image to storage.
23
+ * Always uses scrolling mode — cursor advances past the image.
24
+ */
25
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
26
+ this._storage.addImage(img, this._addImageOpts);
27
+ }
28
+ }
package/src/IIPMetrics.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
 
7
- export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
7
+ export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'image/webp' | 'image/avif' | 'unsupported' | '';
8
8
 
9
9
  export interface IMetrics {
10
10
  mime: ImageType;
@@ -19,16 +19,16 @@ export const UNSUPPORTED_TYPE: IMetrics = {
19
19
  };
20
20
 
21
21
  export function imageType(d: Uint8Array): IMetrics {
22
- if (d.length < 24) {
22
+ if (d.length < 32) {
23
23
  return UNSUPPORTED_TYPE;
24
24
  }
25
- const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
25
+ const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
26
26
  // PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
27
27
  // + first chunk must be IHDR
28
28
  if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
29
29
  return {
30
30
  mime: 'image/png',
31
- width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
31
+ width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
32
32
  height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
33
33
  };
34
34
  }
@@ -41,13 +41,79 @@ export function imageType(d: Uint8Array): IMetrics {
41
41
  if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
42
42
  return {
43
43
  mime: 'image/gif',
44
- width: d[7] << 8 | d[6],
44
+ width: d[7] << 8 | d[6],
45
45
  height: d[9] << 8 | d[8]
46
46
  };
47
47
  }
48
+ // QOI: qoif
49
+ if (d32[0] === 0x66696F71) {
50
+ return {
51
+ mime: 'image/qoi',
52
+ width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
53
+ height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
54
+ };
55
+ }
56
+ // WEBP: RIFF | xxxx | WEBP | VP8x
57
+ if (d32[0] === 0x46464952 && d32[2] === 0x50424557 && (d32[3] & 0xFFFFFF) === 0x385056) {
58
+ switch (d[15]) {
59
+ case 0x58: // Extended WebP VP8X --> "X"
60
+ return {
61
+ mime: 'image/webp',
62
+ width: (d[24] | d[25] << 8 | d[26] << 16) + 1,
63
+ height: (d[27] | d[28] << 8 | d[29] << 16) + 1
64
+ };
65
+ case 0x4C: // Lossless WebP VP8L --> "L"
66
+ if (d[20] !== 0x2f) return UNSUPPORTED_TYPE;
67
+ const dim = d[21] | d[22] << 8 | d[23] << 16 | d[24] << 24;
68
+ return {
69
+ mime: 'image/webp',
70
+ width: (dim & 0x3FFF) + 1,
71
+ height: (dim >>> 14 & 0x3FFF) + 1
72
+ };
73
+ case 0x20: // Lossy WebP VP8 --> " "
74
+ if (d[23] !== 0x9d || d[24] !== 0x01 || d[25] !== 0x2a) return UNSUPPORTED_TYPE;
75
+ return {
76
+ mime: 'image/webp',
77
+ width: (d[26] | d[27] << 8) & 0x3FFF,
78
+ height: (d[28] | d[29] << 8) & 0x3FFF
79
+ };
80
+ }
81
+ return UNSUPPORTED_TYPE;
82
+ }
83
+ // AVIF: Box size | ftyp | avif/avis
84
+ if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
85
+ let pos = -1;
86
+ // search for ispe box within first 1024 bytes
87
+ const limit = Math.min(d.length - 16, 1024);
88
+ for (let i = 8; i < limit; i++) {
89
+ // scan for ispe
90
+ if (d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
91
+ pos = i;
92
+ break;
93
+ }
94
+ }
95
+ if (pos !== -1) {
96
+ // dimensions are in BE at +8 (width) at +12 (height)
97
+ const width =
98
+ d[pos + 8] << 24 |
99
+ d[pos + 9] << 16 |
100
+ d[pos + 10] << 8 |
101
+ d[pos + 11];
102
+ const height =
103
+ d[pos + 12] << 24 |
104
+ d[pos + 13] << 16 |
105
+ d[pos + 14] << 8 |
106
+ d[pos + 15];
107
+ if (width > 0 && height > 0) {
108
+ return { mime: 'image/avif', width, height };
109
+ }
110
+ }
111
+ return UNSUPPORTED_TYPE;
112
+ }
48
113
  return UNSUPPORTED_TYPE;
49
114
  }
50
115
 
116
+
51
117
  function jpgSize(d: Uint8Array): [number, number] {
52
118
  const len = d.length;
53
119
  let i = 4;