@lazyingart/agent-web 0.1.40

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 (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/docs/architecture.md +503 -0
  4. package/package.json +43 -0
  5. package/src/aginti-adapter.js +602 -0
  6. package/src/chat-context.js +1020 -0
  7. package/src/chat-migrations.js +947 -0
  8. package/src/chat-store.js +3308 -0
  9. package/src/cli.js +134 -0
  10. package/src/cloud-server.js +2043 -0
  11. package/src/contracts.js +103 -0
  12. package/src/deterministic-context-summarizer.js +254 -0
  13. package/src/direct-chat-capability-limits.js +66 -0
  14. package/src/direct-chat-contract.js +3 -0
  15. package/src/errors.js +50 -0
  16. package/src/http-contract.js +592 -0
  17. package/src/index.js +88 -0
  18. package/src/localllm-connector.js +667 -0
  19. package/src/migrations.js +231 -0
  20. package/src/operator-health.js +184 -0
  21. package/src/password-verifier.js +131 -0
  22. package/src/service-config.js +547 -0
  23. package/src/service.js +408 -0
  24. package/src/sqlite-health.js +83 -0
  25. package/src/storage-path.js +130 -0
  26. package/src/store.js +914 -0
  27. package/src/validation.js +181 -0
  28. package/src/vision-attachment.js +404 -0
  29. package/src/web/aginti-client.js +552 -0
  30. package/src/web/aginti-protocol.js +1146 -0
  31. package/src/web/asset-map.js +462 -0
  32. package/src/web/browser-app.js +6491 -0
  33. package/src/web/cloud-session-client.js +427 -0
  34. package/src/web/direct-chat-client.js +1482 -0
  35. package/src/web/index.js +10 -0
  36. package/src/web/presentation-state.js +107 -0
  37. package/src/web/pwa-assets.js +854 -0
  38. package/src/web/pwa-update-handoff-store.js +179 -0
  39. package/src/web/safe-rendering.js +836 -0
  40. package/src/web/vision-image-client.js +546 -0
  41. package/src/web/vision-image-sanitizer.js +168 -0
  42. package/src/web/web-release.js +28 -0
@@ -0,0 +1,546 @@
1
+ import { sanitizeVisionImageBytes } from "./vision-image-sanitizer.js";
2
+
3
+ export const BROWSER_VISION_IMAGE_LIMITS = Object.freeze({
4
+ // Files are read and decoded one at a time. This covers current 48 MP phone
5
+ // exports without relaxing the durable 4 MiB attachment contract.
6
+ sourceBytes: 24 * 1024 * 1024,
7
+ sourceMaximumEdge: 8_192,
8
+ sourcePixels: 50 * 1024 * 1024,
9
+ canonicalBytes: 4 * 1024 * 1024,
10
+ maximumEdge: 4_096,
11
+ pixels: 16 * 1024 * 1024,
12
+ previewBytes: 512 * 1024,
13
+ previewMaximumEdge: 512,
14
+ previewPixels: 512 * 512,
15
+ });
16
+
17
+ const ACCEPTED_TYPES = new Set(["image/jpeg", "image/png"]);
18
+ const PNG_SIGNATURE = Object.freeze([137, 80, 78, 71, 13, 10, 26, 10]);
19
+ const ISO_FTYP_BYTES = Object.freeze([0x66, 0x74, 0x79, 0x70]);
20
+ const ISO_FTYP_MAXIMUM_BYTES = 4_096;
21
+ const ISO_FTYP_MAXIMUM_BRANDS = 128;
22
+ const HEIF_STILL_BRANDS = new Set(["heic", "heix", "heim", "heis", "mif1", "mif2"]);
23
+ const HEVC_STILL_BRANDS = new Set(["heic", "heix", "heim", "heis"]);
24
+ const HEIF_SEQUENCE_BRANDS = new Set(["hevc", "hevx", "hevm", "hevs", "msf1"]);
25
+ const AVIF_BRANDS = new Set(["avif", "avis", "MA1A", "MA1B"]);
26
+ const DEFAULT_PREPARATION_TIMEOUT_MS = 15_000;
27
+ const JPEG_SOF_MARKERS = new Set([
28
+ 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7,
29
+ 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
30
+ ]);
31
+
32
+ export class VisionImageInputError extends TypeError {
33
+ constructor(code, message) {
34
+ super(message);
35
+ this.name = "VisionImageInputError";
36
+ this.code = code;
37
+ }
38
+ }
39
+
40
+ function fail(message, code = "invalid_image") {
41
+ throw new VisionImageInputError(code, message);
42
+ }
43
+
44
+ function checkedDimensions(width, height, {
45
+ maximumEdge = BROWSER_VISION_IMAGE_LIMITS.maximumEdge,
46
+ pixels = BROWSER_VISION_IMAGE_LIMITS.pixels,
47
+ message = "image dimensions exceed the safe vision limit",
48
+ } = {}) {
49
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1
50
+ || width > maximumEdge || height > maximumEdge || width * height > pixels) {
51
+ fail(message);
52
+ }
53
+ return Object.freeze({ width, height });
54
+ }
55
+
56
+ function pngDimensions(bytes) {
57
+ if (bytes.byteLength < 24 || PNG_SIGNATURE.some((value, index) => bytes[index] !== value)
58
+ || bytes[12] !== 73 || bytes[13] !== 72 || bytes[14] !== 68 || bytes[15] !== 82) {
59
+ fail("selected file is not a valid PNG image");
60
+ }
61
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
62
+ return Object.freeze({ width: view.getUint32(16), height: view.getUint32(20) });
63
+ }
64
+
65
+ function jpegDimensions(bytes) {
66
+ if (bytes.byteLength < 16 || bytes[0] !== 0xff || bytes[1] !== 0xd8) {
67
+ fail("selected file is not a valid JPEG image");
68
+ }
69
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
70
+ let offset = 2;
71
+ let markers = 0;
72
+ while (offset < bytes.byteLength) {
73
+ if (bytes[offset] !== 0xff) fail("selected JPEG framing is invalid");
74
+ while (offset < bytes.byteLength && bytes[offset] === 0xff) offset += 1;
75
+ if (offset >= bytes.byteLength) break;
76
+ const marker = bytes[offset];
77
+ offset += 1;
78
+ markers += 1;
79
+ if (markers > 65_536 || marker === 0xd9 || marker === 0xda) break;
80
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue;
81
+ if (offset + 2 > bytes.byteLength) break;
82
+ const length = view.getUint16(offset);
83
+ if (length < 2 || offset + length > bytes.byteLength) break;
84
+ if (JPEG_SOF_MARKERS.has(marker)) {
85
+ if (length < 8 || bytes[offset + 2] !== 8) fail("selected JPEG frame is invalid");
86
+ return Object.freeze({ width: view.getUint16(offset + 5), height: view.getUint16(offset + 3) });
87
+ }
88
+ offset += length;
89
+ }
90
+ fail("selected JPEG has no supported frame");
91
+ }
92
+
93
+ export function inspectVisionImageBytes(bytes, mediaType) {
94
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1) fail("image bytes are required");
95
+ if (!ACCEPTED_TYPES.has(mediaType)) fail("image must be JPEG or PNG");
96
+ const dimensions = mediaType === "image/png" ? pngDimensions(bytes) : jpegDimensions(bytes);
97
+ return checkedDimensions(dimensions.width, dimensions.height);
98
+ }
99
+
100
+ export { sanitizeVisionImageBytes };
101
+
102
+ function canvasBlob(canvas, mediaType, quality, operation) {
103
+ return operation.race(new Promise((resolve, reject) => {
104
+ try {
105
+ canvas.toBlob((blob) => {
106
+ if (!blob) reject(new TypeError("the browser could not canonicalize this image"));
107
+ else resolve(blob);
108
+ }, mediaType, quality);
109
+ } catch (error) { reject(error); }
110
+ }));
111
+ }
112
+
113
+ function hasSignature(bytes, signature, offset = 0) {
114
+ return bytes.byteLength >= offset + signature.length
115
+ && signature.every((value, index) => bytes[offset + index] === value);
116
+ }
117
+
118
+ function isoBrand(bytes, offset) {
119
+ return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
120
+ }
121
+
122
+ function inspectIsoFileType(bytes) {
123
+ if (bytes.byteLength < 16) fail("selected HEIC/HEIF file type box is truncated", "malformed_heif");
124
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
125
+ const shortSize = view.getUint32(0);
126
+ let headerBytes = 8;
127
+ let boxBytes = shortSize;
128
+ if (shortSize === 1) {
129
+ if (bytes.byteLength < 24 || typeof view.getBigUint64 !== "function") {
130
+ fail("selected HEIC/HEIF file type box is malformed", "malformed_heif");
131
+ }
132
+ const largeSize = view.getBigUint64(8);
133
+ if (largeSize > BigInt(Number.MAX_SAFE_INTEGER)) {
134
+ fail("selected HEIC/HEIF file type box is too large", "malformed_heif");
135
+ }
136
+ headerBytes = 16;
137
+ boxBytes = Number(largeSize);
138
+ } else if (shortSize === 0) {
139
+ fail("selected HEIC/HEIF file type box has an unbounded size", "malformed_heif");
140
+ }
141
+ const fixedPayloadBytes = 8;
142
+ if (!Number.isSafeInteger(boxBytes) || boxBytes < headerBytes + fixedPayloadBytes
143
+ || boxBytes > ISO_FTYP_MAXIMUM_BYTES || boxBytes > bytes.byteLength
144
+ || (boxBytes - headerBytes - fixedPayloadBytes) % 4 !== 0) {
145
+ fail("selected HEIC/HEIF file type box is malformed", "malformed_heif");
146
+ }
147
+ const compatibleBrandCount = (boxBytes - headerBytes - fixedPayloadBytes) / 4;
148
+ if (compatibleBrandCount > ISO_FTYP_MAXIMUM_BRANDS) {
149
+ fail("selected HEIC/HEIF file declares too many compatibility brands", "malformed_heif");
150
+ }
151
+ const brands = new Set([isoBrand(bytes, headerBytes)]);
152
+ for (let offset = headerBytes + fixedPayloadBytes; offset < boxBytes; offset += 4) {
153
+ brands.add(isoBrand(bytes, offset));
154
+ }
155
+ const hasHevcStill = [...HEVC_STILL_BRANDS].some((brand) => brands.has(brand));
156
+ const hasStill = [...HEIF_STILL_BRANDS].some((brand) => brands.has(brand));
157
+ const hasSequence = [...HEIF_SEQUENCE_BRANDS].some((brand) => brands.has(brand));
158
+ const hasAvif = [...AVIF_BRANDS].some((brand) => brands.has(brand));
159
+ if (Number(hasHevcStill) + Number(hasSequence) + Number(hasAvif) > 1) {
160
+ fail("selected ISO media file has conflicting still-image brands", "conflicting_image_brands");
161
+ }
162
+ if (hasAvif) fail("AVIF input is not supported; choose HEIC, HEIF, JPEG, or PNG", "unsupported_avif");
163
+ if (hasSequence) {
164
+ fail("HEIC/HEIF image sequences are not supported; export one still image as HEIC, JPEG, or PNG", "unsupported_heif_sequence");
165
+ }
166
+ if (!hasStill) fail("selected ISO media file is not a supported HEIC/HEIF still image", "unsupported_image_type");
167
+ return Object.freeze({
168
+ sourceKind: "heif",
169
+ decodeMediaType: hasHevcStill ? "image/heic" : "image/heif",
170
+ canonicalMediaType: "image/jpeg",
171
+ });
172
+ }
173
+
174
+ export function classifyVisionImageSource(bytes) {
175
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1) fail("image bytes are required");
176
+ if (bytes.byteLength >= 2 && bytes[0] === 0xff && bytes[1] === 0xd8) {
177
+ return Object.freeze({
178
+ sourceKind: "jpeg",
179
+ decodeMediaType: "image/jpeg",
180
+ canonicalMediaType: "image/jpeg",
181
+ });
182
+ }
183
+ if (hasSignature(bytes, PNG_SIGNATURE)) {
184
+ return Object.freeze({
185
+ sourceKind: "png",
186
+ decodeMediaType: "image/png",
187
+ canonicalMediaType: "image/png",
188
+ });
189
+ }
190
+ if (hasSignature(bytes, ISO_FTYP_BYTES, 4)) return inspectIsoFileType(bytes);
191
+ fail("selected file bytes are not a supported JPEG, PNG, HEIC, or HEIF still image", "unsupported_image_type");
192
+ }
193
+
194
+ function inspectSourceVisionImageBytes(bytes, mediaType) {
195
+ const dimensions = mediaType === "image/png" ? pngDimensions(bytes) : jpegDimensions(bytes);
196
+ return checkedDimensions(dimensions.width, dimensions.height, {
197
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.sourceMaximumEdge,
198
+ pixels: BROWSER_VISION_IMAGE_LIMITS.sourcePixels,
199
+ message: "source image dimensions exceed the safe decode limit",
200
+ });
201
+ }
202
+
203
+ function createBoundedOperation({
204
+ signal,
205
+ timeoutMs,
206
+ setTimeoutImpl,
207
+ clearTimeoutImpl,
208
+ }) {
209
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000
210
+ || typeof setTimeoutImpl !== "function" || typeof clearTimeoutImpl !== "function") {
211
+ fail("image preparation timing limits are invalid", "invalid_image_limits");
212
+ }
213
+ if (signal !== undefined && signal !== null
214
+ && (typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function"
215
+ || typeof signal.removeEventListener !== "function")) {
216
+ fail("image preparation cancellation signal is invalid", "invalid_image_limits");
217
+ }
218
+ let reason = null;
219
+ let finished = false;
220
+ const cancellationWaiters = new Set();
221
+ const cancel = (error) => {
222
+ if (finished || reason !== null) return;
223
+ reason = error;
224
+ for (const waiter of cancellationWaiters) waiter(error);
225
+ cancellationWaiters.clear();
226
+ };
227
+ const externalAbort = () => cancel(new VisionImageInputError(
228
+ "image_preparation_aborted",
229
+ "Image preparation was cancelled before any photo was sent.",
230
+ ));
231
+ signal?.addEventListener("abort", externalAbort, { once: true });
232
+ if (signal?.aborted) externalAbort();
233
+ const timer = setTimeoutImpl(() => cancel(new VisionImageInputError(
234
+ "image_preparation_timeout",
235
+ "Image preparation timed out before any photo was sent. Try a smaller photo or export it as JPEG.",
236
+ )), timeoutMs);
237
+ return Object.freeze({
238
+ get cancelled() { return reason !== null; },
239
+ get reason() { return reason; },
240
+ throwIfCancelled() {
241
+ if (reason !== null) throw reason;
242
+ },
243
+ race(value, { lateResolve } = {}) {
244
+ if (reason !== null) return Promise.reject(reason);
245
+ return new Promise((resolve, reject) => {
246
+ let settled = false;
247
+ const finish = (handler, result) => {
248
+ if (settled) return;
249
+ settled = true;
250
+ cancellationWaiters.delete(onCancel);
251
+ handler(result);
252
+ };
253
+ const onCancel = (error) => finish(reject, error);
254
+ cancellationWaiters.add(onCancel);
255
+ Promise.resolve(value).then(
256
+ (result) => {
257
+ if (settled) {
258
+ try { lateResolve?.(result); } catch { /* Late resource cleanup is best effort. */ }
259
+ return;
260
+ }
261
+ finish(resolve, result);
262
+ },
263
+ (error) => finish(reject, error),
264
+ );
265
+ });
266
+ },
267
+ finish() {
268
+ if (finished) return;
269
+ finished = true;
270
+ clearTimeoutImpl(timer);
271
+ signal?.removeEventListener("abort", externalAbort);
272
+ cancellationWaiters.clear();
273
+ },
274
+ });
275
+ }
276
+
277
+ function normalizedDecodeBlob(file, bytes, mediaType, BlobImpl) {
278
+ if (typeof BlobImpl !== "function") fail("browser image decoding is unavailable", "decode_unavailable");
279
+ const declaredType = typeof file.type === "string" ? file.type.trim().toLowerCase() : "";
280
+ if (file instanceof BlobImpl && declaredType === mediaType) return file;
281
+ try { return new BlobImpl([bytes], { type: mediaType }); }
282
+ catch { fail("browser image decoding is unavailable", "decode_unavailable"); }
283
+ }
284
+
285
+ function fittedDimensions(width, height, { maximumEdge, pixels }) {
286
+ const scale = Math.min(1, maximumEdge / width, maximumEdge / height, Math.sqrt(pixels / (width * height)));
287
+ return checkedDimensions(
288
+ Math.max(1, Math.floor(width * scale)),
289
+ Math.max(1, Math.floor(height * scale)),
290
+ { maximumEdge, pixels },
291
+ );
292
+ }
293
+
294
+ function renderBitmap(document, canvas, bitmap, dimensions, mediaType) {
295
+ canvas.width = dimensions.width;
296
+ canvas.height = dimensions.height;
297
+ const context = canvas.getContext?.("2d", { alpha: mediaType === "image/png" });
298
+ if (!context || typeof context.drawImage !== "function"
299
+ || (mediaType === "image/jpeg" && typeof context.fillRect !== "function")) {
300
+ fail("browser image canonicalization is unavailable");
301
+ }
302
+ if (mediaType === "image/jpeg") {
303
+ context.fillStyle = "#ffffff";
304
+ context.fillRect(0, 0, dimensions.width, dimensions.height);
305
+ }
306
+ context.drawImage(bitmap, 0, 0, dimensions.width, dimensions.height);
307
+ return context;
308
+ }
309
+
310
+ function smallerDimensions(dimensions, encodedBytes, byteLimit, limits) {
311
+ const scale = Math.min(0.82, Math.max(0.25, Math.sqrt(byteLimit / encodedBytes) * 0.9));
312
+ let width = Math.max(1, Math.floor(dimensions.width * scale));
313
+ let height = Math.max(1, Math.floor(dimensions.height * scale));
314
+ if (width === dimensions.width && width > 1) width -= 1;
315
+ if (height === dimensions.height && height > 1) height -= 1;
316
+ return checkedDimensions(width, height, limits);
317
+ }
318
+
319
+ async function boundedCanvasEncoding(document, canvas, bitmap, mediaType, initialDimensions, {
320
+ byteLimit,
321
+ maximumEdge,
322
+ pixels,
323
+ errorMessage,
324
+ }, operation) {
325
+ let dimensions = initialDimensions;
326
+ for (let geometryAttempt = 0; geometryAttempt < 6; geometryAttempt += 1) {
327
+ operation.throwIfCancelled();
328
+ renderBitmap(document, canvas, bitmap, dimensions, mediaType);
329
+ const qualities = mediaType === "image/jpeg" ? [0.9, 0.76, 0.62] : [undefined];
330
+ let lastBlob;
331
+ for (const quality of qualities) {
332
+ const blob = await canvasBlob(canvas, mediaType, quality, operation);
333
+ if (blob.type !== mediaType || blob.size < 1) fail("the browser returned an invalid canonical image");
334
+ if (blob.size <= byteLimit) return Object.freeze({ blob, dimensions });
335
+ lastBlob = blob;
336
+ }
337
+ if (geometryAttempt === 5) break;
338
+ dimensions = smallerDimensions(dimensions, lastBlob.size, byteLimit, { maximumEdge, pixels });
339
+ }
340
+ fail(errorMessage);
341
+ }
342
+
343
+ async function sanitizedCanvasResult(encoded, mediaType, expectedDimensions, byteLimit, errorMessage, operation) {
344
+ const canvasBytes = new Uint8Array(await operation.race(encoded.arrayBuffer()));
345
+ if (canvasBytes.byteLength !== encoded.size) fail("canonical image changed while it was read");
346
+ const bytes = sanitizeVisionImageBytes(canvasBytes, mediaType);
347
+ if (bytes.byteLength > byteLimit) fail(errorMessage);
348
+ const dimensions = inspectVisionImageBytes(bytes, mediaType);
349
+ if (dimensions.width !== expectedDimensions.width || dimensions.height !== expectedDimensions.height) {
350
+ fail("canonical image dimensions changed unexpectedly");
351
+ }
352
+ return Object.freeze({ bytes, dimensions });
353
+ }
354
+
355
+ async function decodeVisionSource(sourceBlob, sourceKind, {
356
+ document,
357
+ createImageBitmapImpl,
358
+ createObjectUrl,
359
+ revokeObjectUrl,
360
+ operation,
361
+ }) {
362
+ const closeDrawable = (drawable) => {
363
+ try { drawable?.close?.(); } catch { /* Native decoder resource cleanup is best effort. */ }
364
+ };
365
+ if (typeof createImageBitmapImpl === "function") {
366
+ try {
367
+ const drawable = await operation.race(
368
+ createImageBitmapImpl(sourceBlob, { imageOrientation: "from-image" }),
369
+ { lateResolve: closeDrawable },
370
+ );
371
+ return Object.freeze({ drawable, release: () => closeDrawable(drawable) });
372
+ } catch (error) {
373
+ if (operation.cancelled) throw operation.reason ?? error;
374
+ // Older Safari exposes createImageBitmap but rejects the options overload.
375
+ try {
376
+ const drawable = await operation.race(
377
+ createImageBitmapImpl(sourceBlob),
378
+ { lateResolve: closeDrawable },
379
+ );
380
+ return Object.freeze({ drawable, release: () => closeDrawable(drawable) });
381
+ } catch (fallbackError) {
382
+ if (operation.cancelled) throw operation.reason ?? fallbackError;
383
+ // Fall through to the native HTML image decoder.
384
+ }
385
+ }
386
+ }
387
+ if (typeof createObjectUrl !== "function" || typeof revokeObjectUrl !== "function") {
388
+ if (sourceKind === "heif") {
389
+ fail(
390
+ "This browser cannot decode this HEIC/HEIF photo natively. Choose it again from Photos, or export/share it as JPEG or PNG and retry.",
391
+ "heif_decode_unavailable",
392
+ );
393
+ }
394
+ fail("browser image decoding is unavailable", "decode_unavailable");
395
+ }
396
+ let objectUrl;
397
+ let image;
398
+ try {
399
+ operation.throwIfCancelled();
400
+ objectUrl = createObjectUrl(sourceBlob);
401
+ if (typeof objectUrl !== "string" || objectUrl.length < 1) fail("browser image decoding is unavailable");
402
+ image = document.createElement("img");
403
+ if (!image || typeof image.decode !== "function") fail("browser image decoding is unavailable");
404
+ image.decoding = "async";
405
+ image.src = objectUrl;
406
+ await operation.race(Promise.resolve().then(() => image.decode()));
407
+ let released = false;
408
+ return Object.freeze({
409
+ drawable: image,
410
+ release() {
411
+ if (released) return;
412
+ released = true;
413
+ image.removeAttribute?.("src");
414
+ try { revokeObjectUrl(objectUrl); } catch { /* Native decoder resource cleanup is best effort. */ }
415
+ },
416
+ });
417
+ } catch (error) {
418
+ image?.removeAttribute?.("src");
419
+ if (typeof objectUrl === "string" && objectUrl.length > 0) {
420
+ try { revokeObjectUrl(objectUrl); } catch { /* Best-effort cleanup after decode failure. */ }
421
+ }
422
+ if (operation.cancelled) throw operation.reason ?? error;
423
+ if (sourceKind === "heif") {
424
+ fail(
425
+ "This browser could not decode this HEIC/HEIF photo. Choose it again from Photos, or export/share it as JPEG or PNG and retry.",
426
+ "heif_decode_unavailable",
427
+ );
428
+ }
429
+ fail("the browser could not decode this JPEG or PNG image safely", "decode_failed");
430
+ }
431
+ }
432
+
433
+ export async function canonicalizeVisionImage(file, {
434
+ document = globalThis.document,
435
+ createImageBitmapImpl = globalThis.createImageBitmap,
436
+ createObjectUrl = globalThis.URL?.createObjectURL?.bind(globalThis.URL),
437
+ revokeObjectUrl = globalThis.URL?.revokeObjectURL?.bind(globalThis.URL),
438
+ BlobImpl = globalThis.Blob,
439
+ makeAttachmentId,
440
+ signal,
441
+ timeoutMs = DEFAULT_PREPARATION_TIMEOUT_MS,
442
+ setTimeoutImpl = globalThis.setTimeout,
443
+ clearTimeoutImpl = globalThis.clearTimeout,
444
+ } = {}) {
445
+ if (!file || typeof file.arrayBuffer !== "function" || !Number.isSafeInteger(file.size)
446
+ || file.size < 1 || file.size > BROWSER_VISION_IMAGE_LIMITS.sourceBytes) {
447
+ fail("select a JPEG, PNG, HEIC, or HEIF still image up to 24 MiB");
448
+ }
449
+ if (typeof document?.createElement !== "function" || typeof makeAttachmentId !== "function") {
450
+ fail("browser image canonicalization is unavailable");
451
+ }
452
+ const operation = createBoundedOperation({ signal, timeoutMs, setTimeoutImpl, clearTimeoutImpl });
453
+ try {
454
+ const source = new Uint8Array(await operation.race(file.arrayBuffer()));
455
+ if (source.byteLength !== file.size) fail("selected image changed while it was read");
456
+ const classification = classifyVisionImageSource(source);
457
+ const declared = classification.sourceKind === "heif"
458
+ ? null
459
+ : inspectSourceVisionImageBytes(source, classification.decodeMediaType);
460
+ const decodeBlob = normalizedDecodeBlob(file, source, classification.decodeMediaType, BlobImpl);
461
+ let decodedResource;
462
+ try {
463
+ decodedResource = await decodeVisionSource(decodeBlob, classification.sourceKind, {
464
+ document,
465
+ createImageBitmapImpl,
466
+ createObjectUrl,
467
+ revokeObjectUrl,
468
+ operation,
469
+ });
470
+ const drawable = decodedResource.drawable;
471
+ const decodedWidth = Number.isSafeInteger(drawable?.naturalWidth) && drawable.naturalWidth > 0
472
+ ? drawable.naturalWidth : drawable?.width;
473
+ const decodedHeight = Number.isSafeInteger(drawable?.naturalHeight) && drawable.naturalHeight > 0
474
+ ? drawable.naturalHeight : drawable?.height;
475
+ const decoded = checkedDimensions(decodedWidth, decodedHeight, {
476
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.sourceMaximumEdge,
477
+ pixels: BROWSER_VISION_IMAGE_LIMITS.sourcePixels,
478
+ message: "decoded image dimensions exceed the safe decode limit",
479
+ });
480
+ if (declared !== null) {
481
+ const sameGeometry = decoded.width === declared.width && decoded.height === declared.height;
482
+ const metadataOrientedGeometry = decoded.width === declared.height && decoded.height === declared.width;
483
+ if (!sameGeometry && !metadataOrientedGeometry) {
484
+ fail("decoded image dimensions do not match its file header");
485
+ }
486
+ }
487
+ const canvas = document.createElement("canvas");
488
+ const mediaType = classification.canonicalMediaType;
489
+ const canonicalTarget = fittedDimensions(decoded.width, decoded.height, {
490
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.maximumEdge,
491
+ pixels: BROWSER_VISION_IMAGE_LIMITS.pixels,
492
+ });
493
+ const canonicalEncoding = await boundedCanvasEncoding(document, canvas, drawable, mediaType, canonicalTarget, {
494
+ byteLimit: BROWSER_VISION_IMAGE_LIMITS.canonicalBytes,
495
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.maximumEdge,
496
+ pixels: BROWSER_VISION_IMAGE_LIMITS.pixels,
497
+ errorMessage: "canonical image exceeds 4 MiB after safe downscaling",
498
+ }, operation);
499
+ const canonical = await sanitizedCanvasResult(
500
+ canonicalEncoding.blob,
501
+ mediaType,
502
+ canonicalEncoding.dimensions,
503
+ BROWSER_VISION_IMAGE_LIMITS.canonicalBytes,
504
+ "canonical image exceeds 4 MiB after safe downscaling",
505
+ operation,
506
+ );
507
+ let preview = canonical;
508
+ if (canonical.dimensions.width > BROWSER_VISION_IMAGE_LIMITS.previewMaximumEdge
509
+ || canonical.dimensions.height > BROWSER_VISION_IMAGE_LIMITS.previewMaximumEdge
510
+ || canonical.bytes.byteLength > BROWSER_VISION_IMAGE_LIMITS.previewBytes) {
511
+ const previewTarget = fittedDimensions(decoded.width, decoded.height, {
512
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.previewMaximumEdge,
513
+ pixels: BROWSER_VISION_IMAGE_LIMITS.previewPixels,
514
+ });
515
+ const previewEncoding = await boundedCanvasEncoding(document, canvas, drawable, mediaType, previewTarget, {
516
+ byteLimit: BROWSER_VISION_IMAGE_LIMITS.previewBytes,
517
+ maximumEdge: BROWSER_VISION_IMAGE_LIMITS.previewMaximumEdge,
518
+ pixels: BROWSER_VISION_IMAGE_LIMITS.previewPixels,
519
+ errorMessage: "the browser could not create a bounded image preview",
520
+ }, operation);
521
+ preview = await sanitizedCanvasResult(
522
+ previewEncoding.blob,
523
+ mediaType,
524
+ previewEncoding.dimensions,
525
+ BROWSER_VISION_IMAGE_LIMITS.previewBytes,
526
+ "the browser could not create a bounded image preview",
527
+ operation,
528
+ );
529
+ }
530
+ operation.throwIfCancelled();
531
+ return Object.freeze({
532
+ attachmentId: makeAttachmentId("image"),
533
+ mediaType,
534
+ byteLength: canonical.bytes.byteLength,
535
+ width: canonical.dimensions.width,
536
+ height: canonical.dimensions.height,
537
+ bytes: canonical.bytes,
538
+ previewBlob: new BlobImpl([preview.bytes], { type: mediaType }),
539
+ });
540
+ } finally {
541
+ decodedResource?.release();
542
+ }
543
+ } finally {
544
+ operation.finish();
545
+ }
546
+ }