@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199

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.
Files changed (80) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  3. package/src/lib/app-utils/docs-help.generated.js +1 -0
  4. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  5. package/src/lib/app-utils/docs-index.generated.js +3 -3
  6. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  7. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  8. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  9. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  10. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  11. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  12. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  13. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  14. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  15. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  16. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  17. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  18. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  19. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  20. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  21. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  22. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  23. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  24. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  25. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  26. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  27. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  28. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  29. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  30. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  31. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  32. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  33. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  35. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  36. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  38. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  39. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  41. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  42. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  44. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  45. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  47. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  48. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  50. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  51. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  53. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  54. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  56. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  57. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  59. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  60. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  62. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  63. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  65. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  66. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  68. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  69. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  71. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  72. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  73. package/src/lib/app-utils/name-search.d.ts +12 -0
  74. package/src/lib/app-utils/name-search.js +11 -0
  75. package/src/lib/app-utils/name-search.js.map +1 -1
  76. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  77. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  78. package/src/server.d.ts +1 -0
  79. package/src/server.js +7 -0
  80. package/src/server.js.map +1 -1
@@ -0,0 +1,417 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The tags of a WebM or Matroska film (AGL-3331). Read-only, like MP4.
18
+ *
19
+ * Matroska is EBML (RFC 8794): every element is a variable-length ID, a
20
+ * variable-length size and a payload. The file is an EBML header naming the
21
+ * DocType, then one `Segment` whose top-level children include `Info`
22
+ * (title, date, muxer) and `Tags`, and — the bulk of the file — `Cluster`s
23
+ * of media (RFC 9559 §5.1).
24
+ *
25
+ * ## Reading without downloading the film
26
+ *
27
+ * Every read goes through the random-access reader and is sized exactly: an
28
+ * element header is read in at most two pieces (see `headerAt`), so even the
29
+ * header of a `Cluster` costs its own few bytes and nothing of its payload.
30
+ * The Segment's children are walked from the start until the first
31
+ * `Cluster`; anything that lives past the media — `Tags` usually does,
32
+ * because a muxer writes them last — is reached through the `SeekHead`
33
+ * index (RFC 9559 §5.1.1), whose positions count from the start of the
34
+ * Segment's payload. A Segment of unknown size (a live recording, which is
35
+ * what a browser's MediaRecorder writes) runs to the end of the file, and a
36
+ * `Cluster` of unknown size simply ends the walk.
37
+ *
38
+ * ## What is read
39
+ *
40
+ * - `Info` (§5.1.2): `Title`, `DateUTC` (signed nanoseconds since
41
+ * 2001-01-01T00:00:00Z), `WritingApp` — or `MuxingApp` when that is all
42
+ * there is — as the encoder.
43
+ * - `Tags` (§5.1.8): the `SimpleTag`s of every `Tag` that targets the whole
44
+ * file — no track, edition, chapter or attachment UID — preferring target
45
+ * level 50, the film itself. A per-track tag (ffmpeg writes a `DURATION`
46
+ * for every track) describes a stream, not the file, and is skipped.
47
+ * Nested SimpleTags are not read.
48
+ *
49
+ * Tags outrank `Info` for the same idea: `DATE_RECORDED` says when the
50
+ * recording began, `DateUTC` only when the file was muxed.
51
+ */ import { otherEmbeddedKey } from "../media-embedded-fields.js";
52
+ // Element IDs, with their length-marker bits, as the specifications list them.
53
+ const ID_EBML = 0x1a45dfa3;
54
+ const ID_DOCTYPE = 0x4282;
55
+ const ID_SEGMENT = 0x18538067;
56
+ const ID_SEEKHEAD = 0x114d9b74;
57
+ const ID_SEEK = 0x4dbb;
58
+ const ID_SEEKID = 0x53ab;
59
+ const ID_SEEKPOSITION = 0x53ac;
60
+ const ID_INFO = 0x1549a966;
61
+ const ID_TITLE = 0x7ba9;
62
+ const ID_DATEUTC = 0x4461;
63
+ const ID_MUXINGAPP = 0x4d80;
64
+ const ID_WRITINGAPP = 0x5741;
65
+ const ID_TAGS = 0x1254c367;
66
+ const ID_TAG = 0x7373;
67
+ const ID_TARGETS = 0x63c0;
68
+ const ID_TARGETTYPEVALUE = 0x68ca;
69
+ const ID_TAG_UIDS = new Set([
70
+ 0x63c5,
71
+ 0x63c9,
72
+ 0x63c4,
73
+ 0x63c6
74
+ ]);
75
+ const ID_SIMPLETAG = 0x67c8;
76
+ const ID_TAGNAME = 0x45a3;
77
+ const ID_TAGSTRING = 0x4487;
78
+ const ID_CLUSTER = 0x1f43b675;
79
+ const EBML_HEADER_MAX_BYTES = 4096;
80
+ const SEEKHEAD_MAX_BYTES = 1024 * 1024;
81
+ const INFO_MAX_BYTES = 1024 * 1024;
82
+ const TAGS_MAX_BYTES = 8 * 1024 * 1024;
83
+ /** Top-level elements ahead of the first Cluster: a handful, in practice. */ const MAX_TOP_LEVEL = 256;
84
+ const MAX_CHILDREN = 65536;
85
+ /** Milliseconds from 1970 to 2001-01-01T00:00:00Z, Matroska's epoch. */ const EPOCH_2001_MS = 978307200000;
86
+ /** The length of a VINT from its first byte's leading zeros (RFC 8794 §4). */ function vintLength(first) {
87
+ for(let length = 1, mask = 0x80; length <= 8; length++, mask >>= 1){
88
+ if (first & mask) return length;
89
+ }
90
+ return 0;
91
+ }
92
+ /** An element ID: 1 to 4 bytes, marker bit kept (RFC 8794 §5). */ function readId(bytes, at) {
93
+ var _bytes_at, _bytes_;
94
+ const length = vintLength((_bytes_at = bytes[at]) != null ? _bytes_at : 0);
95
+ if (!length || length > 4 || at + length > bytes.length) return null;
96
+ let id = 0;
97
+ for(let index = 0; index < length; index++)id = id * 256 + ((_bytes_ = bytes[at + index]) != null ? _bytes_ : 0);
98
+ return {
99
+ id,
100
+ length
101
+ };
102
+ }
103
+ /**
104
+ * An element data size: 1 to 8 bytes, marker bit dropped; every value bit
105
+ * set means "unknown" (RFC 8794 §6.2). A size past 2^53 is refused.
106
+ */ function readSize(bytes, at) {
107
+ var _bytes_at;
108
+ const first = (_bytes_at = bytes[at]) != null ? _bytes_at : 0;
109
+ const length = vintLength(first);
110
+ if (!length || at + length > bytes.length) return null;
111
+ let value = first & 0xff >> length;
112
+ let allOnes = value === 0xff >> length;
113
+ for(let index = 1; index < length; index++){
114
+ var _bytes_;
115
+ const byte = (_bytes_ = bytes[at + index]) != null ? _bytes_ : 0;
116
+ value = value * 256 + byte;
117
+ if (byte !== 0xff) allOnes = false;
118
+ }
119
+ if (allOnes) return {
120
+ size: null,
121
+ length
122
+ };
123
+ return Number.isSafeInteger(value) ? {
124
+ size: value,
125
+ length
126
+ } : null;
127
+ }
128
+ /** The elements inside `bytes[start, end)`, clamped to it. */ function elements(bytes, start, end) {
129
+ const out = [];
130
+ let at = start;
131
+ while(at < end && out.length < MAX_CHILDREN){
132
+ const id = readId(bytes, at);
133
+ if (!id) break;
134
+ const size = readSize(bytes, at + id.length);
135
+ if (!size) break;
136
+ const body = at + id.length + size.length;
137
+ if (body > end) break;
138
+ // An unknown size inside a buffer can only run to the buffer's end.
139
+ const length = size.size === null ? end - body : Math.min(size.size, end - body);
140
+ out.push({
141
+ id: id.id,
142
+ start: at,
143
+ body,
144
+ size: length
145
+ });
146
+ at = body + length;
147
+ }
148
+ return out;
149
+ }
150
+ const payload = (bytes, element)=>{
151
+ var _element_size;
152
+ return bytes.subarray(element.body, element.body + ((_element_size = element.size) != null ? _element_size : 0));
153
+ };
154
+ /** A Matroska string: UTF-8, possibly zero-padded (RFC 8794 §7.4, §7.5). */ const text = (bytes)=>new TextDecoder('utf-8').decode(bytes).replace(/\0+$/, '');
155
+ function unsigned(bytes) {
156
+ if (bytes.length > 8) return null;
157
+ let value = 0;
158
+ for (const byte of bytes)value = value * 256 + byte;
159
+ return Number.isSafeInteger(value) ? value : null;
160
+ }
161
+ /**
162
+ * One element header read through the reader in at most two sized pieces:
163
+ * five bytes — a 4-byte ID and the first byte of the size, which says how
164
+ * long the size is — then whatever of the size is still missing. For a
165
+ * `Cluster` (a 4-byte ID) the first read is all header; for a shorter ID it
166
+ * may run into that element's own payload, never into a Cluster's — so no
167
+ * read reaches into the media this module means to skip.
168
+ */ async function headerAt(reader, at) {
169
+ var _held_;
170
+ if (at < 0 || at >= reader.size) return null;
171
+ const head = await reader.read(at, Math.min(reader.size, at + 5));
172
+ const id = readId(head, 0);
173
+ if (!id) return null;
174
+ const sizeAt = at + id.length;
175
+ const held = head.subarray(id.length);
176
+ const length = vintLength((_held_ = held[0]) != null ? _held_ : 0);
177
+ if (!held.length || !length) return null;
178
+ let sizeBytes = held.subarray(0, length);
179
+ if (held.length < length) {
180
+ const rest = await reader.read(sizeAt + held.length, sizeAt + length);
181
+ if (held.length + rest.length < length) return null;
182
+ sizeBytes = new Uint8Array(length);
183
+ sizeBytes.set(held);
184
+ sizeBytes.set(rest, held.length);
185
+ }
186
+ const size = readSize(sizeBytes, 0);
187
+ if (!size) return null;
188
+ return {
189
+ id: id.id,
190
+ start: at,
191
+ body: sizeAt + length,
192
+ size: size.size
193
+ };
194
+ }
195
+ /** Official tag names (Matroska Media Container Tag Specifications). */ const TAG_KEYS = {
196
+ TITLE: 'title',
197
+ ARTIST: 'creator',
198
+ DESCRIPTION: 'description',
199
+ COMMENT: 'comment',
200
+ DATE_RECORDED: 'createdAt',
201
+ COPYRIGHT: 'copyright',
202
+ KEYWORDS: 'keywords',
203
+ ENCODER: 'encoder'
204
+ };
205
+ /**
206
+ * A tag date — `YYYY-MM-DD hh:mm:ss.mss`, any tail of it omitted — in the
207
+ * catalog's form. The tag specification names no zone, so none is added.
208
+ */ function normalizeTagDate(raw) {
209
+ const value = raw.trim();
210
+ const match = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?)(?:[.,](\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
211
+ if (!match) return value;
212
+ const [, date, time, fraction, zone] = match;
213
+ return `${date}T${time}${fraction ? `.${fraction.slice(0, 3)}` : ''}${zone != null ? zone : ''}`;
214
+ }
215
+ let Collector = class Collector {
216
+ add(key, value, rank, label) {
217
+ const trimmed = value.trim();
218
+ if (!trimmed) return;
219
+ let candidate;
220
+ if (key === 'keywords') {
221
+ const items = trimmed.split(/[,;]/).map((item)=>item.trim()).filter(Boolean);
222
+ if (!items.length) return;
223
+ candidate = {
224
+ key,
225
+ value: items,
226
+ source: 'webm',
227
+ editable: false
228
+ };
229
+ } else if (key === 'creator') {
230
+ candidate = {
231
+ key,
232
+ value: [
233
+ trimmed
234
+ ],
235
+ source: 'webm',
236
+ editable: false
237
+ };
238
+ } else if (key === 'createdAt') {
239
+ candidate = {
240
+ key,
241
+ value: normalizeTagDate(trimmed),
242
+ source: 'webm',
243
+ editable: false
244
+ };
245
+ } else {
246
+ candidate = {
247
+ key,
248
+ value: trimmed,
249
+ source: 'webm',
250
+ editable: false
251
+ };
252
+ if (label) candidate.label = label;
253
+ }
254
+ const held = this.byKey.get(key);
255
+ if (!held || rank < held.rank) this.byKey.set(key, {
256
+ candidate,
257
+ rank
258
+ });
259
+ }
260
+ candidates() {
261
+ return [
262
+ ...this.byKey.values()
263
+ ].map(({ candidate })=>candidate);
264
+ }
265
+ constructor(){
266
+ this.byKey = new Map();
267
+ }
268
+ };
269
+ /** Ranks: a film-level tag, another whole-file tag, then Info. */ const RANK_TAG_FILM = 0;
270
+ const RANK_TAG_OTHER = 10;
271
+ const RANK_INFO = 20;
272
+ function readTags(bytes, out) {
273
+ for (const tag of elements(bytes, 0, bytes.length)){
274
+ var _tag_size;
275
+ if (tag.id !== ID_TAG) continue;
276
+ const parts = elements(bytes, tag.body, tag.body + ((_tag_size = tag.size) != null ? _tag_size : 0));
277
+ const targets = parts.find((part)=>part.id === ID_TARGETS);
278
+ let level = 50;
279
+ let wholeFile = true;
280
+ if (targets) {
281
+ var _targets_size;
282
+ for (const target of elements(bytes, targets.body, targets.body + ((_targets_size = targets.size) != null ? _targets_size : 0))){
283
+ const value = unsigned(payload(bytes, target));
284
+ if (target.id === ID_TARGETTYPEVALUE && value !== null) level = value;
285
+ if (ID_TAG_UIDS.has(target.id) && value !== 0) wholeFile = false;
286
+ }
287
+ }
288
+ if (!wholeFile) continue;
289
+ const rank = level === 50 ? RANK_TAG_FILM : RANK_TAG_OTHER;
290
+ for (const simple of parts){
291
+ var _simple_size;
292
+ if (simple.id !== ID_SIMPLETAG) continue;
293
+ let name = '';
294
+ let value = null;
295
+ for (const field of elements(bytes, simple.body, simple.body + ((_simple_size = simple.size) != null ? _simple_size : 0))){
296
+ if (field.id === ID_TAGNAME) name = text(payload(bytes, field)).trim();
297
+ else if (field.id === ID_TAGSTRING) value = text(payload(bytes, field));
298
+ }
299
+ if (!name || value === null) continue;
300
+ const key = TAG_KEYS[name.toUpperCase()];
301
+ if (key) out.add(key, value, rank);
302
+ else out.add(otherEmbeddedKey('webm', name), value, rank, name);
303
+ }
304
+ }
305
+ }
306
+ function readInfo(bytes, out) {
307
+ let muxingApp = '';
308
+ let writingApp = '';
309
+ for (const field of elements(bytes, 0, bytes.length)){
310
+ const value = payload(bytes, field);
311
+ if (field.id === ID_TITLE) out.add('title', text(value), RANK_INFO);
312
+ else if (field.id === ID_MUXINGAPP) muxingApp = text(value).trim();
313
+ else if (field.id === ID_WRITINGAPP) writingApp = text(value).trim();
314
+ else if (field.id === ID_DATEUTC && value.length === 8) {
315
+ const nanoseconds = new DataView(value.buffer, value.byteOffset, value.byteLength).getBigInt64(0);
316
+ const date = new Date(EPOCH_2001_MS + Number(nanoseconds / 1000000n));
317
+ if (!Number.isNaN(date.getTime())) {
318
+ out.add('createdAt', date.toISOString().replace(/\.\d{3}Z$/, 'Z'), RANK_INFO);
319
+ }
320
+ }
321
+ }
322
+ const encoder = writingApp || muxingApp;
323
+ if (encoder) out.add('encoder', encoder, RANK_INFO);
324
+ if (muxingApp && writingApp && muxingApp !== writingApp) {
325
+ out.add(otherEmbeddedKey('webm', 'MuxingApp'), muxingApp, RANK_INFO, 'Muxing application');
326
+ }
327
+ }
328
+ /**
329
+ * Read a film's tags through random access.
330
+ *
331
+ * Null when the bytes do not open with an EBML header whose DocType is
332
+ * `webm` or `matroska`. Every candidate is `editable: false`.
333
+ */ export async function readWebm(reader) {
334
+ const ebml = await headerAt(reader, 0);
335
+ if (!ebml || ebml.id !== ID_EBML || ebml.size === null) return null;
336
+ if (ebml.size > EBML_HEADER_MAX_BYTES) return null;
337
+ const header = await reader.read(ebml.body, ebml.body + ebml.size);
338
+ const docType = elements(header, 0, header.length).find((element)=>element.id === ID_DOCTYPE);
339
+ const kind = docType ? text(payload(header, docType)) : '';
340
+ if (kind !== 'webm' && kind !== 'matroska') return null;
341
+ // The Segment follows the header, perhaps after a Void.
342
+ let segment = null;
343
+ let at = ebml.body + ebml.size;
344
+ for(let count = 0; count < 16; count++){
345
+ const element = await headerAt(reader, at);
346
+ if (!element) break;
347
+ if (element.id === ID_SEGMENT) {
348
+ segment = element;
349
+ break;
350
+ }
351
+ if (element.size === null) break;
352
+ at = element.body + element.size;
353
+ }
354
+ const out = new Collector();
355
+ if (!segment) return {
356
+ candidates: []
357
+ };
358
+ const origin = segment.body;
359
+ const segmentEnd = segment.size === null ? reader.size : Math.min(reader.size, segment.body + segment.size);
360
+ const visited = new Set();
361
+ const infos = [];
362
+ const tags = [];
363
+ const pending = [];
364
+ const visit = async (element)=>{
365
+ if (visited.has(element.start) || element.size === null) return;
366
+ const cap = element.id === ID_SEEKHEAD ? SEEKHEAD_MAX_BYTES : element.id === ID_INFO ? INFO_MAX_BYTES : element.id === ID_TAGS ? TAGS_MAX_BYTES : 0;
367
+ if (!cap || element.size > cap) return;
368
+ visited.add(element.start);
369
+ const bytes = await reader.read(element.body, Math.min(segmentEnd, element.body + element.size));
370
+ if (element.id === ID_INFO) infos.push(bytes);
371
+ else if (element.id === ID_TAGS) tags.push(bytes);
372
+ else {
373
+ for (const seek of elements(bytes, 0, bytes.length)){
374
+ var _seek_size;
375
+ if (seek.id !== ID_SEEK) continue;
376
+ let id = null;
377
+ let position = null;
378
+ for (const field of elements(bytes, seek.body, seek.body + ((_seek_size = seek.size) != null ? _seek_size : 0))){
379
+ const value = payload(bytes, field);
380
+ if (field.id === ID_SEEKID) id = unsigned(value);
381
+ else if (field.id === ID_SEEKPOSITION) position = unsigned(value);
382
+ }
383
+ if (id !== null && position !== null && (id === ID_INFO || id === ID_TAGS || id === ID_SEEKHEAD)) {
384
+ pending.push({
385
+ id,
386
+ at: origin + position
387
+ });
388
+ }
389
+ }
390
+ }
391
+ };
392
+ // Walk the Segment's children up to the first Cluster.
393
+ at = origin;
394
+ for(let count = 0; count < MAX_TOP_LEVEL && at < segmentEnd; count++){
395
+ const element = await headerAt(reader, at);
396
+ if (!element || element.id === ID_CLUSTER || element.size === null) break;
397
+ await visit(element);
398
+ at = element.body + element.size;
399
+ }
400
+ // Then what the index says lives beyond it — a second SeekHead included,
401
+ // one level deep, which is as far as the specification nests them.
402
+ for(let round = 0; round < 2 && pending.length; round++){
403
+ const targets = pending.splice(0);
404
+ for (const target of targets){
405
+ if (visited.has(target.at) || target.at >= segmentEnd) continue;
406
+ const element = await headerAt(reader, target.at);
407
+ if (element && element.id === target.id) await visit(element);
408
+ }
409
+ }
410
+ for (const bytes of tags)readTags(bytes, out);
411
+ for (const bytes of infos)readInfo(bytes, out);
412
+ return {
413
+ candidates: out.candidates()
414
+ };
415
+ }
416
+
417
+ //# sourceMappingURL=webm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/app-utils/media-embedded-metadata/webm.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The tags of a WebM or Matroska film (AGL-3331). Read-only, like MP4.\n *\n * Matroska is EBML (RFC 8794): every element is a variable-length ID, a\n * variable-length size and a payload. The file is an EBML header naming the\n * DocType, then one `Segment` whose top-level children include `Info`\n * (title, date, muxer) and `Tags`, and — the bulk of the file — `Cluster`s\n * of media (RFC 9559 §5.1).\n *\n * ## Reading without downloading the film\n *\n * Every read goes through the random-access reader and is sized exactly: an\n * element header is read in at most two pieces (see `headerAt`), so even the\n * header of a `Cluster` costs its own few bytes and nothing of its payload.\n * The Segment's children are walked from the start until the first\n * `Cluster`; anything that lives past the media — `Tags` usually does,\n * because a muxer writes them last — is reached through the `SeekHead`\n * index (RFC 9559 §5.1.1), whose positions count from the start of the\n * Segment's payload. A Segment of unknown size (a live recording, which is\n * what a browser's MediaRecorder writes) runs to the end of the file, and a\n * `Cluster` of unknown size simply ends the walk.\n *\n * ## What is read\n *\n * - `Info` (§5.1.2): `Title`, `DateUTC` (signed nanoseconds since\n * 2001-01-01T00:00:00Z), `WritingApp` — or `MuxingApp` when that is all\n * there is — as the encoder.\n * - `Tags` (§5.1.8): the `SimpleTag`s of every `Tag` that targets the whole\n * file — no track, edition, chapter or attachment UID — preferring target\n * level 50, the film itself. A per-track tag (ffmpeg writes a `DURATION`\n * for every track) describes a stream, not the file, and is skipped.\n * Nested SimpleTags are not read.\n *\n * Tags outrank `Info` for the same idea: `DATE_RECORDED` says when the\n * recording began, `DateUTC` only when the file was muxed.\n */\n\nimport {\n otherEmbeddedKey,\n type MediaEmbeddedCanonicalKey,\n} from '../media-embedded-fields'\nimport type { EmbeddedByteReader, EmbeddedCandidate } from './types'\n\n// Element IDs, with their length-marker bits, as the specifications list them.\nconst ID_EBML = 0x1a45dfa3\nconst ID_DOCTYPE = 0x4282\nconst ID_SEGMENT = 0x18538067\nconst ID_SEEKHEAD = 0x114d9b74\nconst ID_SEEK = 0x4dbb\nconst ID_SEEKID = 0x53ab\nconst ID_SEEKPOSITION = 0x53ac\nconst ID_INFO = 0x1549a966\nconst ID_TITLE = 0x7ba9\nconst ID_DATEUTC = 0x4461\nconst ID_MUXINGAPP = 0x4d80\nconst ID_WRITINGAPP = 0x5741\nconst ID_TAGS = 0x1254c367\nconst ID_TAG = 0x7373\nconst ID_TARGETS = 0x63c0\nconst ID_TARGETTYPEVALUE = 0x68ca\nconst ID_TAG_UIDS = new Set([0x63c5, 0x63c9, 0x63c4, 0x63c6])\nconst ID_SIMPLETAG = 0x67c8\nconst ID_TAGNAME = 0x45a3\nconst ID_TAGSTRING = 0x4487\nconst ID_CLUSTER = 0x1f43b675\n\nconst EBML_HEADER_MAX_BYTES = 4096\nconst SEEKHEAD_MAX_BYTES = 1024 * 1024\nconst INFO_MAX_BYTES = 1024 * 1024\nconst TAGS_MAX_BYTES = 8 * 1024 * 1024\n/** Top-level elements ahead of the first Cluster: a handful, in practice. */\nconst MAX_TOP_LEVEL = 256\nconst MAX_CHILDREN = 65536\n/** Milliseconds from 1970 to 2001-01-01T00:00:00Z, Matroska's epoch. */\nconst EPOCH_2001_MS = 978307200000\n\ninterface ElementHeader {\n id: number\n start: number\n /** Where the payload begins. */\n body: number\n /** The payload size; null for the \"unknown\" size (all value bits set). */\n size: number | null\n}\n\n/** The length of a VINT from its first byte's leading zeros (RFC 8794 §4). */\nfunction vintLength(first: number): number {\n for (let length = 1, mask = 0x80; length <= 8; length++, mask >>= 1) {\n if (first & mask) return length\n }\n return 0\n}\n\n/** An element ID: 1 to 4 bytes, marker bit kept (RFC 8794 §5). */\nfunction readId(\n bytes: Uint8Array,\n at: number,\n): { id: number; length: number } | null {\n const length = vintLength(bytes[at] ?? 0)\n if (!length || length > 4 || at + length > bytes.length) return null\n let id = 0\n for (let index = 0; index < length; index++)\n id = id * 256 + (bytes[at + index] ?? 0)\n return { id, length }\n}\n\n/**\n * An element data size: 1 to 8 bytes, marker bit dropped; every value bit\n * set means \"unknown\" (RFC 8794 §6.2). A size past 2^53 is refused.\n */\nfunction readSize(\n bytes: Uint8Array,\n at: number,\n): { size: number | null; length: number } | null {\n const first = bytes[at] ?? 0\n const length = vintLength(first)\n if (!length || at + length > bytes.length) return null\n let value = first & (0xff >> length)\n let allOnes = value === 0xff >> length\n for (let index = 1; index < length; index++) {\n const byte = bytes[at + index] ?? 0\n value = value * 256 + byte\n if (byte !== 0xff) allOnes = false\n }\n if (allOnes) return { size: null, length }\n return Number.isSafeInteger(value) ? { size: value, length } : null\n}\n\n/** The elements inside `bytes[start, end)`, clamped to it. */\nfunction elements(\n bytes: Uint8Array,\n start: number,\n end: number,\n): ElementHeader[] {\n const out: ElementHeader[] = []\n let at = start\n while (at < end && out.length < MAX_CHILDREN) {\n const id = readId(bytes, at)\n if (!id) break\n const size = readSize(bytes, at + id.length)\n if (!size) break\n const body = at + id.length + size.length\n if (body > end) break\n // An unknown size inside a buffer can only run to the buffer's end.\n const length =\n size.size === null ? end - body : Math.min(size.size, end - body)\n out.push({ id: id.id, start: at, body, size: length })\n at = body + length\n }\n return out\n}\n\nconst payload = (bytes: Uint8Array, element: ElementHeader) =>\n bytes.subarray(element.body, element.body + (element.size ?? 0))\n\n/** A Matroska string: UTF-8, possibly zero-padded (RFC 8794 §7.4, §7.5). */\nconst text = (bytes: Uint8Array) =>\n new TextDecoder('utf-8').decode(bytes).replace(/\\0+$/, '')\n\nfunction unsigned(bytes: Uint8Array): number | null {\n if (bytes.length > 8) return null\n let value = 0\n for (const byte of bytes) value = value * 256 + byte\n return Number.isSafeInteger(value) ? value : null\n}\n\n/**\n * One element header read through the reader in at most two sized pieces:\n * five bytes — a 4-byte ID and the first byte of the size, which says how\n * long the size is — then whatever of the size is still missing. For a\n * `Cluster` (a 4-byte ID) the first read is all header; for a shorter ID it\n * may run into that element's own payload, never into a Cluster's — so no\n * read reaches into the media this module means to skip.\n */\nasync function headerAt(\n reader: EmbeddedByteReader,\n at: number,\n): Promise<ElementHeader | null> {\n if (at < 0 || at >= reader.size) return null\n const head = await reader.read(at, Math.min(reader.size, at + 5))\n const id = readId(head, 0)\n if (!id) return null\n const sizeAt = at + id.length\n const held = head.subarray(id.length)\n const length = vintLength(held[0] ?? 0)\n if (!held.length || !length) return null\n let sizeBytes = held.subarray(0, length)\n if (held.length < length) {\n const rest = await reader.read(sizeAt + held.length, sizeAt + length)\n if (held.length + rest.length < length) return null\n sizeBytes = new Uint8Array(length)\n sizeBytes.set(held)\n sizeBytes.set(rest, held.length)\n }\n const size = readSize(sizeBytes, 0)\n if (!size) return null\n return { id: id.id, start: at, body: sizeAt + length, size: size.size }\n}\n\ntype VideoKey = Extract<\n MediaEmbeddedCanonicalKey,\n | 'title'\n | 'creator'\n | 'description'\n | 'comment'\n | 'createdAt'\n | 'copyright'\n | 'keywords'\n | 'encoder'\n>\n\n/** Official tag names (Matroska Media Container Tag Specifications). */\nconst TAG_KEYS: Record<string, VideoKey> = {\n TITLE: 'title',\n ARTIST: 'creator',\n DESCRIPTION: 'description',\n COMMENT: 'comment',\n DATE_RECORDED: 'createdAt',\n COPYRIGHT: 'copyright',\n KEYWORDS: 'keywords',\n ENCODER: 'encoder',\n}\n\n/**\n * A tag date — `YYYY-MM-DD hh:mm:ss.mss`, any tail of it omitted — in the\n * catalog's form. The tag specification names no zone, so none is added.\n */\nfunction normalizeTagDate(raw: string): string {\n const value = raw.trim()\n const match =\n /^(\\d{4}-\\d{2}-\\d{2})[T ](\\d{2}:\\d{2}(?::\\d{2})?)(?:[.,](\\d+))?(Z|[+-]\\d{2}:\\d{2})?$/.exec(\n value,\n )\n if (!match) return value\n const [, date, time, fraction, zone] = match\n return `${date}T${time}${fraction ? `.${fraction.slice(0, 3)}` : ''}${zone ?? ''}`\n}\n\nclass Collector {\n private readonly byKey = new Map<\n string,\n { candidate: EmbeddedCandidate; rank: number }\n >()\n\n add(key: string, value: string, rank: number, label?: string): void {\n const trimmed = value.trim()\n if (!trimmed) return\n let candidate: EmbeddedCandidate\n if (key === 'keywords') {\n const items = trimmed\n .split(/[,;]/)\n .map((item) => item.trim())\n .filter(Boolean)\n if (!items.length) return\n candidate = { key, value: items, source: 'webm', editable: false }\n } else if (key === 'creator') {\n candidate = { key, value: [trimmed], source: 'webm', editable: false }\n } else if (key === 'createdAt') {\n candidate = {\n key,\n value: normalizeTagDate(trimmed),\n source: 'webm',\n editable: false,\n }\n } else {\n candidate = { key, value: trimmed, source: 'webm', editable: false }\n if (label) candidate.label = label\n }\n const held = this.byKey.get(key)\n if (!held || rank < held.rank) this.byKey.set(key, { candidate, rank })\n }\n\n candidates(): EmbeddedCandidate[] {\n return [...this.byKey.values()].map(({ candidate }) => candidate)\n }\n}\n\n/** Ranks: a film-level tag, another whole-file tag, then Info. */\nconst RANK_TAG_FILM = 0\nconst RANK_TAG_OTHER = 10\nconst RANK_INFO = 20\n\nfunction readTags(bytes: Uint8Array, out: Collector): void {\n for (const tag of elements(bytes, 0, bytes.length)) {\n if (tag.id !== ID_TAG) continue\n const parts = elements(bytes, tag.body, tag.body + (tag.size ?? 0))\n const targets = parts.find((part) => part.id === ID_TARGETS)\n let level = 50\n let wholeFile = true\n if (targets) {\n for (const target of elements(\n bytes,\n targets.body,\n targets.body + (targets.size ?? 0),\n )) {\n const value = unsigned(payload(bytes, target))\n if (target.id === ID_TARGETTYPEVALUE && value !== null) level = value\n if (ID_TAG_UIDS.has(target.id) && value !== 0) wholeFile = false\n }\n }\n if (!wholeFile) continue\n const rank = level === 50 ? RANK_TAG_FILM : RANK_TAG_OTHER\n for (const simple of parts) {\n if (simple.id !== ID_SIMPLETAG) continue\n let name = ''\n let value: string | null = null\n for (const field of elements(\n bytes,\n simple.body,\n simple.body + (simple.size ?? 0),\n )) {\n if (field.id === ID_TAGNAME) name = text(payload(bytes, field)).trim()\n else if (field.id === ID_TAGSTRING) value = text(payload(bytes, field))\n }\n if (!name || value === null) continue\n const key = TAG_KEYS[name.toUpperCase()]\n if (key) out.add(key, value, rank)\n else out.add(otherEmbeddedKey('webm', name), value, rank, name)\n }\n }\n}\n\nfunction readInfo(bytes: Uint8Array, out: Collector): void {\n let muxingApp = ''\n let writingApp = ''\n for (const field of elements(bytes, 0, bytes.length)) {\n const value = payload(bytes, field)\n if (field.id === ID_TITLE) out.add('title', text(value), RANK_INFO)\n else if (field.id === ID_MUXINGAPP) muxingApp = text(value).trim()\n else if (field.id === ID_WRITINGAPP) writingApp = text(value).trim()\n else if (field.id === ID_DATEUTC && value.length === 8) {\n const nanoseconds = new DataView(\n value.buffer,\n value.byteOffset,\n value.byteLength,\n ).getBigInt64(0)\n const date = new Date(EPOCH_2001_MS + Number(nanoseconds / 1_000_000n))\n if (!Number.isNaN(date.getTime())) {\n out.add(\n 'createdAt',\n date.toISOString().replace(/\\.\\d{3}Z$/, 'Z'),\n RANK_INFO,\n )\n }\n }\n }\n const encoder = writingApp || muxingApp\n if (encoder) out.add('encoder', encoder, RANK_INFO)\n if (muxingApp && writingApp && muxingApp !== writingApp) {\n out.add(\n otherEmbeddedKey('webm', 'MuxingApp'),\n muxingApp,\n RANK_INFO,\n 'Muxing application',\n )\n }\n}\n\n/**\n * Read a film's tags through random access.\n *\n * Null when the bytes do not open with an EBML header whose DocType is\n * `webm` or `matroska`. Every candidate is `editable: false`.\n */\nexport async function readWebm(\n reader: EmbeddedByteReader,\n): Promise<{ candidates: EmbeddedCandidate[] } | null> {\n const ebml = await headerAt(reader, 0)\n if (!ebml || ebml.id !== ID_EBML || ebml.size === null) return null\n if (ebml.size > EBML_HEADER_MAX_BYTES) return null\n const header = await reader.read(ebml.body, ebml.body + ebml.size)\n const docType = elements(header, 0, header.length).find(\n (element) => element.id === ID_DOCTYPE,\n )\n const kind = docType ? text(payload(header, docType)) : ''\n if (kind !== 'webm' && kind !== 'matroska') return null\n\n // The Segment follows the header, perhaps after a Void.\n let segment: ElementHeader | null = null\n let at = ebml.body + ebml.size\n for (let count = 0; count < 16; count++) {\n const element = await headerAt(reader, at)\n if (!element) break\n if (element.id === ID_SEGMENT) {\n segment = element\n break\n }\n if (element.size === null) break\n at = element.body + element.size\n }\n const out = new Collector()\n if (!segment) return { candidates: [] }\n const origin = segment.body\n const segmentEnd =\n segment.size === null\n ? reader.size\n : Math.min(reader.size, segment.body + segment.size)\n\n const visited = new Set<number>()\n const infos: Uint8Array[] = []\n const tags: Uint8Array[] = []\n const pending: Array<{ id: number; at: number }> = []\n\n const visit = async (element: ElementHeader) => {\n if (visited.has(element.start) || element.size === null) return\n const cap =\n element.id === ID_SEEKHEAD\n ? SEEKHEAD_MAX_BYTES\n : element.id === ID_INFO\n ? INFO_MAX_BYTES\n : element.id === ID_TAGS\n ? TAGS_MAX_BYTES\n : 0\n if (!cap || element.size > cap) return\n visited.add(element.start)\n const bytes = await reader.read(\n element.body,\n Math.min(segmentEnd, element.body + element.size),\n )\n if (element.id === ID_INFO) infos.push(bytes)\n else if (element.id === ID_TAGS) tags.push(bytes)\n else {\n for (const seek of elements(bytes, 0, bytes.length)) {\n if (seek.id !== ID_SEEK) continue\n let id: number | null = null\n let position: number | null = null\n for (const field of elements(\n bytes,\n seek.body,\n seek.body + (seek.size ?? 0),\n )) {\n const value = payload(bytes, field)\n if (field.id === ID_SEEKID) id = unsigned(value)\n else if (field.id === ID_SEEKPOSITION) position = unsigned(value)\n }\n if (\n id !== null &&\n position !== null &&\n (id === ID_INFO || id === ID_TAGS || id === ID_SEEKHEAD)\n ) {\n pending.push({ id, at: origin + position })\n }\n }\n }\n }\n\n // Walk the Segment's children up to the first Cluster.\n at = origin\n for (let count = 0; count < MAX_TOP_LEVEL && at < segmentEnd; count++) {\n const element = await headerAt(reader, at)\n if (!element || element.id === ID_CLUSTER || element.size === null) break\n await visit(element)\n at = element.body + element.size\n }\n // Then what the index says lives beyond it — a second SeekHead included,\n // one level deep, which is as far as the specification nests them.\n for (let round = 0; round < 2 && pending.length; round++) {\n const targets = pending.splice(0)\n for (const target of targets) {\n if (visited.has(target.at) || target.at >= segmentEnd) continue\n const element = await headerAt(reader, target.at)\n if (element && element.id === target.id) await visit(element)\n }\n }\n\n for (const bytes of tags) readTags(bytes, out)\n for (const bytes of infos) readInfo(bytes, out)\n return { candidates: out.candidates() }\n}\n"],"names":["otherEmbeddedKey","ID_EBML","ID_DOCTYPE","ID_SEGMENT","ID_SEEKHEAD","ID_SEEK","ID_SEEKID","ID_SEEKPOSITION","ID_INFO","ID_TITLE","ID_DATEUTC","ID_MUXINGAPP","ID_WRITINGAPP","ID_TAGS","ID_TAG","ID_TARGETS","ID_TARGETTYPEVALUE","ID_TAG_UIDS","Set","ID_SIMPLETAG","ID_TAGNAME","ID_TAGSTRING","ID_CLUSTER","EBML_HEADER_MAX_BYTES","SEEKHEAD_MAX_BYTES","INFO_MAX_BYTES","TAGS_MAX_BYTES","MAX_TOP_LEVEL","MAX_CHILDREN","EPOCH_2001_MS","vintLength","first","length","mask","readId","bytes","at","id","index","readSize","value","allOnes","byte","size","Number","isSafeInteger","elements","start","end","out","body","Math","min","push","payload","element","subarray","text","TextDecoder","decode","replace","unsigned","headerAt","reader","held","head","read","sizeAt","sizeBytes","rest","Uint8Array","set","TAG_KEYS","TITLE","ARTIST","DESCRIPTION","COMMENT","DATE_RECORDED","COPYRIGHT","KEYWORDS","ENCODER","normalizeTagDate","raw","trim","match","exec","date","time","fraction","zone","slice","Collector","add","key","rank","label","trimmed","candidate","items","split","map","item","filter","Boolean","source","editable","byKey","get","candidates","values","Map","RANK_TAG_FILM","RANK_TAG_OTHER","RANK_INFO","readTags","tag","parts","targets","find","part","level","wholeFile","target","has","simple","name","field","toUpperCase","readInfo","muxingApp","writingApp","nanoseconds","DataView","buffer","byteOffset","byteLength","getBigInt64","Date","isNaN","getTime","toISOString","encoder","readWebm","ebml","header","docType","kind","segment","count","origin","segmentEnd","visited","infos","tags","pending","visit","cap","seek","position","round","splice"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ImageBlockUpdate, type ImageBlocks } from './image-blocks';
18
+ /**
19
+ * Reads the metadata blocks of a WebP. Returns `null` when the bytes are not
20
+ * a WebP; a damaged file yields what came before the damage.
21
+ */
22
+ export declare function readWebpBlocks(bytes: Uint8Array): ImageBlocks | null;
23
+ /**
24
+ * Writes an update into a WebP and returns the new file.
25
+ *
26
+ * An existing `EXIF` or `XMP ` chunk is replaced in place (later duplicates
27
+ * removed); a new one goes after the image — EXIF before any existing XMP,
28
+ * XMP after EXIF — as the extended format orders them. `null` removes every
29
+ * copy. The `VP8X` EXIF and XMP flags are set to match what the file then
30
+ * holds; ICC, alpha and animation flags are left as they were.
31
+ *
32
+ * Throws `EmbeddedWriteError` for a file that is not a WebP or is truncated,
33
+ * for a layout that is neither simple nor extended, for a simple file whose
34
+ * bitstream header cannot give the canvas size a conversion needs, and for
35
+ * an IRB or PNG text update.
36
+ */
37
+ export declare function writeWebpBlocks(bytes: Uint8Array, update: ImageBlockUpdate): Uint8Array;