@standardagents/builder 0.10.0 → 0.10.1-dev.616ec2e

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.
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Server-side image processing for Cloudflare Workers
3
3
  *
4
+ * Uses @standardagents/sip for memory-efficient processing with scanline-based
5
+ * resize and streaming output. Falls back to PNG encoding for transparent images.
6
+ *
4
7
  * Features:
5
- * - Compress/resize images >2MB to fit under 2MB target
6
- * - Convert AVIF/WebP to JPEG/PNG
7
- * - Preserve PNG transparency (don't convert transparent PNGs to JPEG)
8
+ * - Compress/resize images to fit under 1.5MB target
9
+ * - Convert AVIF/WebP/PNG/JPEG to JPEG (default) or PNG (for transparency)
10
+ * - Memory-efficient scanline processing
8
11
  * - Smart quality optimization: quality first, dimensions last
9
12
  */
10
13
  interface ProcessedImage {
@@ -14,7 +17,7 @@ interface ProcessedImage {
14
17
  height: number;
15
18
  }
16
19
  /**
17
- * Process an image to ensure it's under 2MB and in a supported format.
20
+ * Process an image to ensure it's under 1.5MB and in a supported format.
18
21
  *
19
22
  * @param input - Raw image data as ArrayBuffer
20
23
  * @param inputMimeType - MIME type hint (used as fallback for format detection)
@@ -1,133 +1,50 @@
1
- import { PhotonImage, resize, SamplingFilter } from '@cf-wasm/photon/workerd';
1
+ import { probe, sip } from '@standardagents/sip';
2
+ import { decode as decode$1, encode } from '@jsquash/png';
2
3
  import { decode } from '@jsquash/avif';
3
- import { encode } from '@jsquash/png';
4
4
 
5
5
  // src/image-processing/index.ts
6
6
  var MAX_SIZE = 1.5 * 1024 * 1024;
7
- var MAX_DIMENSION = 8192;
7
+ var MAX_DIMENSION = 4096;
8
8
  var MAX_INPUT_SIZE = 20 * 1024 * 1024;
9
9
  async function processImage(input, inputMimeType) {
10
10
  if (input.byteLength > MAX_INPUT_SIZE) {
11
11
  throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);
12
12
  }
13
- const format = detectFormat(input, inputMimeType);
14
- let photonImage;
15
- if (format === "avif") {
16
- const imageData = await decode(input);
17
- photonImage = new PhotonImage(
18
- new Uint8Array(imageData.data.buffer),
19
- imageData.width,
20
- imageData.height
21
- );
22
- } else {
23
- photonImage = PhotonImage.new_from_byteslice(new Uint8Array(input));
13
+ const probeResult = probe(input);
14
+ const format = probeResult.format !== "unknown" ? probeResult.format : detectFormat(input, inputMimeType);
15
+ const hasAlpha = probeResult.hasAlpha;
16
+ if (hasAlpha && (format === "png" || format === "avif" || format === "webp")) {
17
+ return await processPngWithAlpha(input, format);
24
18
  }
25
19
  try {
26
- const width = photonImage.get_width();
27
- const height = photonImage.get_height();
28
- if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
29
- const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
30
- const newWidth = Math.floor(width * scale);
31
- const newHeight = Math.floor(height * scale);
32
- const resized = resize(photonImage, newWidth, newHeight, SamplingFilter.Lanczos3);
33
- photonImage.free();
34
- photonImage = resized;
35
- }
36
- const hasAlpha = detectAlphaChannel(photonImage);
37
- return await encodeWithSizeLimit(photonImage, hasAlpha, MAX_SIZE);
38
- } finally {
39
- photonImage.free();
40
- }
41
- }
42
- function detectFormat(data, mimeType) {
43
- const bytes = new Uint8Array(data.slice(0, 12));
44
- if (bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
45
- const brand = String.fromCharCode(...bytes.slice(8, 12));
46
- if (brand === "avif" || brand === "avis") return "avif";
47
- }
48
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
49
- return "png";
50
- }
51
- if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
52
- return "jpeg";
53
- }
54
- if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
55
- return "webp";
56
- }
57
- if (mimeType.includes("png")) return "png";
58
- if (mimeType.includes("webp")) return "webp";
59
- if (mimeType.includes("avif")) return "avif";
60
- return "jpeg";
61
- }
62
- function detectAlphaChannel(image) {
63
- const rawPixels = image.get_raw_pixels();
64
- for (let i = 3; i < rawPixels.length; i += 4) {
65
- if (rawPixels[i] < 255) {
66
- return true;
67
- }
68
- }
69
- return false;
70
- }
71
- async function encodeWithSizeLimit(image, hasAlpha, maxSize) {
72
- const originalWidth = image.get_width();
73
- const originalHeight = image.get_height();
74
- if (!hasAlpha) {
75
- const qualityLevels = [92, 85, 75, 65, 55, 45];
76
- for (const quality of qualityLevels) {
77
- const encoded3 = image.get_bytes_jpeg(quality);
78
- if (encoded3.byteLength <= maxSize) {
79
- return {
80
- data: encoded3.buffer,
81
- mimeType: "image/jpeg",
82
- width: originalWidth,
83
- height: originalHeight
84
- };
85
- }
86
- }
87
- let scale2 = 0.9;
88
- while (scale2 > 0.15) {
89
- const newWidth = Math.floor(originalWidth * scale2);
90
- const newHeight = Math.floor(originalHeight * scale2);
91
- if (newWidth < 100 || newHeight < 100) {
92
- scale2 *= 0.9;
93
- continue;
94
- }
95
- const resized = resize(image, newWidth, newHeight, SamplingFilter.Lanczos3);
96
- for (const quality of qualityLevels) {
97
- const encoded3 = resized.get_bytes_jpeg(quality);
98
- if (encoded3.byteLength <= maxSize) {
99
- const result2 = {
100
- data: encoded3.buffer,
101
- mimeType: "image/jpeg",
102
- width: newWidth,
103
- height: newHeight
104
- };
105
- resized.free();
106
- return result2;
107
- }
108
- }
109
- resized.free();
110
- scale2 *= 0.9;
111
- }
112
- const finalResized2 = resize(
113
- image,
114
- Math.floor(originalWidth * 0.15),
115
- Math.floor(originalHeight * 0.15),
116
- SamplingFilter.Lanczos3
117
- );
118
- const encoded2 = finalResized2.get_bytes_jpeg(45);
119
- const result = {
120
- data: encoded2.buffer,
20
+ const result = await sip.process(input, {
21
+ maxWidth: MAX_DIMENSION,
22
+ maxHeight: MAX_DIMENSION,
23
+ maxBytes: MAX_SIZE,
24
+ quality: 85
25
+ });
26
+ return {
27
+ data: result.data,
121
28
  mimeType: "image/jpeg",
122
- width: finalResized2.get_width(),
123
- height: finalResized2.get_height()
29
+ width: result.width,
30
+ height: result.height
124
31
  };
125
- finalResized2.free();
126
- return result;
32
+ } catch (err) {
33
+ console.error("[sip] Processing failed, falling back:", err);
34
+ throw err;
35
+ }
36
+ }
37
+ async function processPngWithAlpha(input, format) {
38
+ let imageData;
39
+ if (format === "avif") {
40
+ imageData = await decode(input);
41
+ } else {
42
+ imageData = await decode$1(input);
127
43
  }
128
- const imageData = image.get_image_data();
44
+ const originalWidth = imageData.width;
45
+ const originalHeight = imageData.height;
129
46
  let encoded = await encode(imageData);
130
- if (encoded.byteLength <= maxSize) {
47
+ if (encoded.byteLength <= MAX_SIZE) {
131
48
  return {
132
49
  data: encoded,
133
50
  mimeType: "image/png",
@@ -136,35 +53,42 @@ async function encodeWithSizeLimit(image, hasAlpha, maxSize) {
136
53
  };
137
54
  }
138
55
  let scale = 0.9;
139
- while (encoded.byteLength > maxSize && scale > 0.15) {
56
+ while (encoded.byteLength > MAX_SIZE && scale > 0.15) {
140
57
  const newWidth = Math.floor(originalWidth * scale);
141
58
  const newHeight = Math.floor(originalHeight * scale);
142
59
  if (newWidth < 100 || newHeight < 100) {
143
60
  scale *= 0.9;
144
61
  continue;
145
62
  }
146
- const resized = resize(image, newWidth, newHeight, SamplingFilter.Lanczos3);
147
- const resizedImageData = resized.get_image_data();
63
+ const resized = resizeRgba(
64
+ new Uint8ClampedArray(imageData.data),
65
+ originalWidth,
66
+ originalHeight,
67
+ newWidth,
68
+ newHeight
69
+ );
70
+ const resizedImageData = { data: resized, width: newWidth, height: newHeight };
148
71
  encoded = await encode(resizedImageData);
149
- if (encoded.byteLength <= maxSize) {
150
- const result = {
72
+ if (encoded.byteLength <= MAX_SIZE) {
73
+ return {
151
74
  data: encoded,
152
75
  mimeType: "image/png",
153
76
  width: newWidth,
154
77
  height: newHeight
155
78
  };
156
- resized.free();
157
- return result;
158
79
  }
159
- resized.free();
160
80
  scale *= 0.9;
161
81
  }
162
82
  const finalWidth = Math.floor(originalWidth * 0.15);
163
83
  const finalHeight = Math.floor(originalHeight * 0.15);
164
- const finalResized = resize(image, finalWidth, finalHeight, SamplingFilter.Lanczos3);
165
- const finalImageData = finalResized.get_image_data();
166
- encoded = await encode(finalImageData);
167
- finalResized.free();
84
+ const finalResized = resizeRgba(
85
+ new Uint8ClampedArray(imageData.data),
86
+ originalWidth,
87
+ originalHeight,
88
+ finalWidth,
89
+ finalHeight
90
+ );
91
+ encoded = await encode({ data: finalResized, width: finalWidth, height: finalHeight });
168
92
  return {
169
93
  data: encoded,
170
94
  mimeType: "image/png",
@@ -172,6 +96,58 @@ async function encodeWithSizeLimit(image, hasAlpha, maxSize) {
172
96
  height: finalHeight
173
97
  };
174
98
  }
99
+ function resizeRgba(src, srcWidth, srcHeight, dstWidth, dstHeight) {
100
+ const dst = new Uint8ClampedArray(dstWidth * dstHeight * 4);
101
+ const xScale = srcWidth / dstWidth;
102
+ const yScale = srcHeight / dstHeight;
103
+ for (let dstY = 0; dstY < dstHeight; dstY++) {
104
+ for (let dstX = 0; dstX < dstWidth; dstX++) {
105
+ const srcXFloat = dstX * xScale;
106
+ const srcYFloat = dstY * yScale;
107
+ const srcX0 = Math.floor(srcXFloat);
108
+ const srcY0 = Math.floor(srcYFloat);
109
+ const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
110
+ const srcY1 = Math.min(srcY0 + 1, srcHeight - 1);
111
+ const tx = srcXFloat - srcX0;
112
+ const ty = srcYFloat - srcY0;
113
+ const idx00 = (srcY0 * srcWidth + srcX0) * 4;
114
+ const idx10 = (srcY0 * srcWidth + srcX1) * 4;
115
+ const idx01 = (srcY1 * srcWidth + srcX0) * 4;
116
+ const idx11 = (srcY1 * srcWidth + srcX1) * 4;
117
+ const dstIdx = (dstY * dstWidth + dstX) * 4;
118
+ for (let c = 0; c < 4; c++) {
119
+ const v00 = src[idx00 + c];
120
+ const v10 = src[idx10 + c];
121
+ const v01 = src[idx01 + c];
122
+ const v11 = src[idx11 + c];
123
+ const top = v00 * (1 - tx) + v10 * tx;
124
+ const bottom = v01 * (1 - tx) + v11 * tx;
125
+ dst[dstIdx + c] = Math.round(top * (1 - ty) + bottom * ty);
126
+ }
127
+ }
128
+ }
129
+ return dst;
130
+ }
131
+ function detectFormat(data, mimeType) {
132
+ const bytes = new Uint8Array(data.slice(0, 12));
133
+ if (bytes[4] === 102 && bytes[5] === 116 && bytes[6] === 121 && bytes[7] === 112) {
134
+ const brand = String.fromCharCode(...bytes.slice(8, 12));
135
+ if (brand === "avif" || brand === "avis") return "avif";
136
+ }
137
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
138
+ return "png";
139
+ }
140
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
141
+ return "jpeg";
142
+ }
143
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
144
+ return "webp";
145
+ }
146
+ if (mimeType.includes("png")) return "png";
147
+ if (mimeType.includes("webp")) return "webp";
148
+ if (mimeType.includes("avif")) return "avif";
149
+ return "jpeg";
150
+ }
175
151
  function needsProcessing(data, mimeType) {
176
152
  const binaryLength = Math.ceil(data.length * 3 / 4);
177
153
  return binaryLength > MAX_SIZE || mimeType.includes("avif") || mimeType.includes("webp");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/image-processing/index.ts"],"names":["decodeAvif","encoded","scale","result","finalResized","encodePng"],"mappings":"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","file":"image-processing.js","sourcesContent":["/**\n * Server-side image processing for Cloudflare Workers\n *\n * Features:\n * - Compress/resize images >2MB to fit under 2MB target\n * - Convert AVIF/WebP to JPEG/PNG\n * - Preserve PNG transparency (don't convert transparent PNGs to JPEG)\n * - Smart quality optimization: quality first, dimensions last\n */\n\nimport {\n PhotonImage,\n SamplingFilter,\n resize,\n} from \"@cf-wasm/photon/workerd\";\nimport { decode as decodeAvif } from \"@jsquash/avif\";\nimport { encode as encodeJpeg, decode as decodeJpeg } from \"@jsquash/jpeg\";\nimport { encode as encodePng, decode as decodePng } from \"@jsquash/png\";\n\n// Target 1.5MB binary → ~2MB base64 for RPC transfer\n// SQLite limit is 2MB per row, but base64 overhead means we need headroom\nconst MAX_SIZE = 1.5 * 1024 * 1024;\n\n// Memory safety limit - extremely large images can crash Workers (128MB limit)\nconst MAX_DIMENSION = 8192;\n\n// Reject images that would consume too much memory before decoding\nconst MAX_INPUT_SIZE = 20 * 1024 * 1024; // 20MB\n\nexport interface ProcessedImage {\n data: ArrayBuffer;\n mimeType: \"image/jpeg\" | \"image/png\";\n width: number;\n height: number;\n}\n\n/**\n * Process an image to ensure it's under 2MB and in a supported format.\n *\n * @param input - Raw image data as ArrayBuffer\n * @param inputMimeType - MIME type hint (used as fallback for format detection)\n * @returns Processed image data with updated mimeType and dimensions\n */\nexport async function processImage(\n input: ArrayBuffer,\n inputMimeType: string\n): Promise<ProcessedImage> {\n // Memory safety: reject very large images\n if (input.byteLength > MAX_INPUT_SIZE) {\n throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);\n }\n\n // Detect format from magic bytes\n const format = detectFormat(input, inputMimeType);\n\n // Decode image to PhotonImage\n let photonImage: PhotonImage;\n\n if (format === \"avif\") {\n // AVIF: decode with jSquash, then convert to PhotonImage\n const imageData = await decodeAvif(input);\n photonImage = new PhotonImage(\n new Uint8Array(imageData.data.buffer),\n imageData.width,\n imageData.height\n );\n } else {\n // JPEG, PNG, WebP: photon can decode these directly\n photonImage = PhotonImage.new_from_byteslice(new Uint8Array(input));\n }\n\n try {\n // Get original dimensions\n const width = photonImage.get_width();\n const height = photonImage.get_height();\n\n // Memory safety: limit extreme dimensions\n if (width > MAX_DIMENSION || height > MAX_DIMENSION) {\n const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);\n const newWidth = Math.floor(width * scale);\n const newHeight = Math.floor(height * scale);\n const resized = resize(photonImage, newWidth, newHeight, SamplingFilter.Lanczos3);\n photonImage.free();\n photonImage = resized;\n }\n\n // Check for transparency to decide output format\n const hasAlpha = detectAlphaChannel(photonImage);\n\n // Encode with smart size optimization\n return await encodeWithSizeLimit(photonImage, hasAlpha, MAX_SIZE);\n } finally {\n photonImage.free();\n }\n}\n\n/**\n * Detect image format from magic bytes\n */\nfunction detectFormat(data: ArrayBuffer, mimeType: string): string {\n const bytes = new Uint8Array(data.slice(0, 12));\n\n // AVIF/HEIC: starts with ftyp box\n if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {\n const brand = String.fromCharCode(...bytes.slice(8, 12));\n if (brand === \"avif\" || brand === \"avis\") return \"avif\";\n // Note: HEIC (heic, heix, mif1) not supported - would need separate decoder\n }\n\n // PNG: 89 50 4E 47\n if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {\n return \"png\";\n }\n\n // JPEG: FF D8 FF\n if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {\n return \"jpeg\";\n }\n\n // WebP: RIFF....WEBP\n if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&\n bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n return \"webp\";\n }\n\n // Fallback to mimeType hint\n if (mimeType.includes(\"png\")) return \"png\";\n if (mimeType.includes(\"webp\")) return \"webp\";\n if (mimeType.includes(\"avif\")) return \"avif\";\n return \"jpeg\";\n}\n\n/**\n * Check if image has transparent pixels\n */\nfunction detectAlphaChannel(image: PhotonImage): boolean {\n const rawPixels = image.get_raw_pixels();\n // RGBA format: check every 4th byte (alpha channel)\n for (let i = 3; i < rawPixels.length; i += 4) {\n if (rawPixels[i] < 255) {\n return true; // Found non-opaque pixel\n }\n }\n return false;\n}\n\n/**\n * Encode image with smart size optimization.\n * Strategy: quality reduction first, dimension reduction last.\n */\nasync function encodeWithSizeLimit(\n image: PhotonImage,\n hasAlpha: boolean,\n maxSize: number\n): Promise<ProcessedImage> {\n const originalWidth = image.get_width();\n const originalHeight = image.get_height();\n\n // For opaque images: use JPEG with quality reduction first\n if (!hasAlpha) {\n // Quality levels to try (minimum 45% to preserve acceptable quality)\n const qualityLevels = [92, 85, 75, 65, 55, 45];\n\n // Try each quality level at original resolution\n for (const quality of qualityLevels) {\n const encoded = image.get_bytes_jpeg(quality);\n if (encoded.byteLength <= maxSize) {\n return {\n data: encoded.buffer,\n mimeType: \"image/jpeg\",\n width: originalWidth,\n height: originalHeight,\n };\n }\n }\n\n // Quality 45 wasn't enough - reduce dimensions iteratively\n // Use fine-grained steps (10% reduction each) to find optimal quality/resolution tradeoff\n let scale = 0.9;\n while (scale > 0.15) {\n const newWidth = Math.floor(originalWidth * scale);\n const newHeight = Math.floor(originalHeight * scale);\n\n // Skip if dimensions haven't changed meaningfully\n if (newWidth < 100 || newHeight < 100) {\n scale *= 0.9;\n continue;\n }\n\n const resized = resize(image, newWidth, newHeight, SamplingFilter.Lanczos3);\n\n for (const quality of qualityLevels) {\n const encoded = resized.get_bytes_jpeg(quality);\n if (encoded.byteLength <= maxSize) {\n const result = {\n data: encoded.buffer,\n mimeType: \"image/jpeg\" as const,\n width: newWidth,\n height: newHeight,\n };\n resized.free();\n return result;\n }\n }\n\n resized.free();\n scale *= 0.9; // 10% reduction per step for finer granularity\n }\n\n // Fallback: return smallest we could achieve\n const finalResized = resize(\n image,\n Math.floor(originalWidth * 0.15),\n Math.floor(originalHeight * 0.15),\n SamplingFilter.Lanczos3\n );\n const encoded = finalResized.get_bytes_jpeg(45);\n const result = {\n data: encoded.buffer,\n mimeType: \"image/jpeg\" as const,\n width: finalResized.get_width(),\n height: finalResized.get_height(),\n };\n finalResized.free();\n return result;\n }\n\n // For transparent images: use PNG (lossless, can only reduce dimensions)\n // First try at original resolution\n const imageData = image.get_image_data();\n let encoded = await encodePng(imageData);\n\n if (encoded.byteLength <= maxSize) {\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: originalWidth,\n height: originalHeight,\n };\n }\n\n // PNG too large - reduce dimensions iteratively (10% reduction per step)\n let scale = 0.9;\n while (encoded.byteLength > maxSize && scale > 0.15) {\n const newWidth = Math.floor(originalWidth * scale);\n const newHeight = Math.floor(originalHeight * scale);\n\n // Skip if dimensions are too small\n if (newWidth < 100 || newHeight < 100) {\n scale *= 0.9;\n continue;\n }\n\n const resized = resize(image, newWidth, newHeight, SamplingFilter.Lanczos3);\n const resizedImageData = resized.get_image_data();\n encoded = await encodePng(resizedImageData);\n\n if (encoded.byteLength <= maxSize) {\n const result = {\n data: encoded,\n mimeType: \"image/png\" as const,\n width: newWidth,\n height: newHeight,\n };\n resized.free();\n return result;\n }\n\n resized.free();\n scale *= 0.9;\n }\n\n // Fallback: return smallest PNG we could achieve\n const finalWidth = Math.floor(originalWidth * 0.15);\n const finalHeight = Math.floor(originalHeight * 0.15);\n const finalResized = resize(image, finalWidth, finalHeight, SamplingFilter.Lanczos3);\n const finalImageData = finalResized.get_image_data();\n encoded = await encodePng(finalImageData);\n finalResized.free();\n\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: finalWidth,\n height: finalHeight,\n };\n}\n\n/**\n * Check if an image needs processing based on size and format.\n *\n * @param data - Base64-encoded image data\n * @param mimeType - MIME type of the image\n * @returns true if processing is needed\n */\nexport function needsProcessing(data: string, mimeType: string): boolean {\n // Decode base64 to get actual size\n const binaryLength = Math.ceil(data.length * 3 / 4);\n\n // Process if >2MB or unsupported format\n return (\n binaryLength > MAX_SIZE ||\n mimeType.includes(\"avif\") ||\n mimeType.includes(\"webp\")\n );\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert ArrayBuffer to base64 string\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n"]}
1
+ {"version":3,"sources":["../src/image-processing/index.ts"],"names":["decodeAvif","decodePng","encodePng"],"mappings":";;;;;AA0BA,IAAM,QAAA,GAAW,MAAM,IAAA,GAAO,IAAA;AAG9B,IAAM,aAAA,GAAgB,IAAA;AAGtB,IAAM,cAAA,GAAiB,KAAK,IAAA,GAAO,IAAA;AAgBnC,eAAsB,YAAA,CACpB,OACA,aAAA,EACyB;AAEzB,EAAA,IAAI,KAAA,CAAM,aAAa,cAAA,EAAgB;AACrC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iBAAA,EAAoB,MAAM,UAAU,CAAA,eAAA,EAAkB,cAAc,CAAA,WAAA,CAAa,CAAA;AAAA,EACnG;AAGA,EAAA,MAAM,WAAA,GAAc,MAAM,KAAK,CAAA;AAC/B,EAAA,MAAM,MAAA,GAAS,YAAY,MAAA,KAAW,SAAA,GAAY,YAAY,MAAA,GAAS,YAAA,CAAa,OAAO,aAAa,CAAA;AACxG,EAAA,MAAM,WAAW,WAAA,CAAY,QAAA;AAG7B,EAAA,IAAI,aAAa,MAAA,KAAW,KAAA,IAAS,MAAA,KAAW,MAAA,IAAU,WAAW,MAAA,CAAA,EAAS;AAC5E,IAAA,OAAO,MAAM,mBAAA,CAAoB,KAAA,EAAO,MAAM,CAAA;AAAA,EAChD;AAGA,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO;AAAA,MACtC,QAAA,EAAU,aAAA;AAAA,MACV,SAAA,EAAW,aAAA;AAAA,MACX,QAAA,EAAU,QAAA;AAAA,MACV,OAAA,EAAS;AAAA,KACV,CAAA;AAED,IAAA,OAAO;AAAA,MACL,MAAM,MAAA,CAAO,IAAA;AAAA,MACb,QAAA,EAAU,YAAA;AAAA,MACV,OAAO,MAAA,CAAO,KAAA;AAAA,MACd,QAAQ,MAAA,CAAO;AAAA,KACjB;AAAA,EACF,SAAS,GAAA,EAAK;AAEZ,IAAA,OAAA,CAAQ,KAAA,CAAM,0CAA0C,GAAG,CAAA;AAC3D,IAAA,MAAM,GAAA;AAAA,EACR;AACF;AAKA,eAAe,mBAAA,CACb,OACA,MAAA,EACyB;AAEzB,EAAA,IAAI,SAAA;AAEJ,EAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,IAAA,SAAA,GAAY,MAAMA,OAAW,KAAK,CAAA;AAAA,EACpC,CAAA,MAAO;AACL,IAAA,SAAA,GAAY,MAAMC,SAAU,KAAK,CAAA;AAAA,EACnC;AAEA,EAAA,MAAM,gBAAgB,SAAA,CAAU,KAAA;AAChC,EAAA,MAAM,iBAAiB,SAAA,CAAU,MAAA;AAGjC,EAAA,IAAI,OAAA,GAAU,MAAMC,MAAA,CAAU,SAAS,CAAA;AAEvC,EAAA,IAAI,OAAA,CAAQ,cAAc,QAAA,EAAU;AAClC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,OAAA;AAAA,MACN,QAAA,EAAU,WAAA;AAAA,MACV,KAAA,EAAO,aAAA;AAAA,MACP,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAIA,EAAA,IAAI,KAAA,GAAQ,GAAA;AACZ,EAAA,OAAO,OAAA,CAAQ,UAAA,GAAa,QAAA,IAAY,KAAA,GAAQ,IAAA,EAAM;AACpD,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,aAAA,GAAgB,KAAK,CAAA;AACjD,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,cAAA,GAAiB,KAAK,CAAA;AAEnD,IAAA,IAAI,QAAA,GAAW,GAAA,IAAO,SAAA,GAAY,GAAA,EAAK;AACrC,MAAA,KAAA,IAAS,GAAA;AACT,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,OAAA,GAAU,UAAA;AAAA,MACd,IAAI,iBAAA,CAAkB,SAAA,CAAU,IAAI,CAAA;AAAA,MACpC,aAAA;AAAA,MACA,cAAA;AAAA,MACA,QAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,MAAM,mBAAkC,EAAE,IAAA,EAAM,SAAS,KAAA,EAAO,QAAA,EAAU,QAAQ,SAAA,EAAU;AAC5F,IAAA,OAAA,GAAU,MAAMA,OAAU,gBAAgB,CAAA;AAE1C,IAAA,IAAI,OAAA,CAAQ,cAAc,QAAA,EAAU;AAClC,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,OAAA;AAAA,QACN,QAAA,EAAU,WAAA;AAAA,QACV,KAAA,EAAO,QAAA;AAAA,QACP,MAAA,EAAQ;AAAA,OACV;AAAA,IACF;AAEA,IAAA,KAAA,IAAS,GAAA;AAAA,EACX;AAGA,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,KAAA,CAAM,aAAA,GAAgB,IAAI,CAAA;AAClD,EAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,cAAA,GAAiB,IAAI,CAAA;AACpD,EAAA,MAAM,YAAA,GAAe,UAAA;AAAA,IACnB,IAAI,iBAAA,CAAkB,SAAA,CAAU,IAAI,CAAA;AAAA,IACpC,aAAA;AAAA,IACA,cAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,OAAA,GAAU,MAAMA,OAAU,EAAE,IAAA,EAAM,cAAc,KAAA,EAAO,UAAA,EAAY,MAAA,EAAQ,WAAA,EAAa,CAAA;AAExF,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,QAAA,EAAU,WAAA;AAAA,IACV,KAAA,EAAO,UAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACV;AACF;AAKA,SAAS,UAAA,CACP,GAAA,EACA,QAAA,EACA,SAAA,EACA,UACA,SAAA,EACmB;AACnB,EAAA,MAAM,GAAA,GAAM,IAAI,iBAAA,CAAkB,QAAA,GAAW,YAAY,CAAC,CAAA;AAC1D,EAAA,MAAM,SAAS,QAAA,GAAW,QAAA;AAC1B,EAAA,MAAM,SAAS,SAAA,GAAY,SAAA;AAE3B,EAAA,KAAA,IAAS,IAAA,GAAO,CAAA,EAAG,IAAA,GAAO,SAAA,EAAW,IAAA,EAAA,EAAQ;AAC3C,IAAA,KAAA,IAAS,IAAA,GAAO,CAAA,EAAG,IAAA,GAAO,QAAA,EAAU,IAAA,EAAA,EAAQ;AAC1C,MAAA,MAAM,YAAY,IAAA,GAAO,MAAA;AACzB,MAAA,MAAM,YAAY,IAAA,GAAO,MAAA;AACzB,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAClC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAClC,MAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,GAAQ,CAAA,EAAG,WAAW,CAAC,CAAA;AAC9C,MAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,KAAA,GAAQ,CAAA,EAAG,YAAY,CAAC,CAAA;AAC/C,MAAA,MAAM,KAAK,SAAA,GAAY,KAAA;AACvB,MAAA,MAAM,KAAK,SAAA,GAAY,KAAA;AAEvB,MAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,QAAA,GAAW,KAAA,IAAS,CAAA;AAC3C,MAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,QAAA,GAAW,KAAA,IAAS,CAAA;AAC3C,MAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,QAAA,GAAW,KAAA,IAAS,CAAA;AAC3C,MAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,QAAA,GAAW,KAAA,IAAS,CAAA;AAC3C,MAAA,MAAM,MAAA,GAAA,CAAU,IAAA,GAAO,QAAA,GAAW,IAAA,IAAQ,CAAA;AAE1C,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,QAAA,MAAM,GAAA,GAAM,GAAA,CAAI,KAAA,GAAQ,CAAC,CAAA;AACzB,QAAA,MAAM,GAAA,GAAM,GAAA,CAAI,KAAA,GAAQ,CAAC,CAAA;AACzB,QAAA,MAAM,GAAA,GAAM,GAAA,CAAI,KAAA,GAAQ,CAAC,CAAA;AACzB,QAAA,MAAM,GAAA,GAAM,GAAA,CAAI,KAAA,GAAQ,CAAC,CAAA;AACzB,QAAA,MAAM,GAAA,GAAM,GAAA,IAAO,CAAA,GAAI,EAAA,CAAA,GAAM,GAAA,GAAM,EAAA;AACnC,QAAA,MAAM,MAAA,GAAS,GAAA,IAAO,CAAA,GAAI,EAAA,CAAA,GAAM,GAAA,GAAM,EAAA;AACtC,QAAA,GAAA,CAAI,MAAA,GAAS,CAAC,CAAA,GAAI,IAAA,CAAK,MAAM,GAAA,IAAO,CAAA,GAAI,EAAA,CAAA,GAAM,MAAA,GAAS,EAAE,CAAA;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,GAAA;AACT;AAKA,SAAS,YAAA,CAAa,MAAmB,QAAA,EAA0B;AACjE,EAAA,MAAM,QAAQ,IAAI,UAAA,CAAW,KAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA;AAE9C,EAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,MAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,KAAA,CAAM,CAAC,MAAM,GAAA,EAAM;AACpF,IAAA,MAAM,KAAA,GAAQ,OAAO,YAAA,CAAa,GAAG,MAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA;AACvD,IAAA,IAAI,KAAA,KAAU,MAAA,IAAU,KAAA,KAAU,MAAA,EAAQ,OAAO,MAAA;AAAA,EACnD;AAEA,EAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,MAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,CAAC,MAAM,EAAA,EAAM;AACpF,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,EAAM;AAC/D,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,MAAM,CAAC,CAAA,KAAM,EAAA,IAC5E,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,CAAC,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,EAAE,CAAA,KAAM,EAAA,IAAQ,KAAA,CAAM,EAAE,MAAM,EAAA,EAAM;AACtF,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,IAAI,QAAA,CAAS,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,KAAA;AACrC,EAAA,IAAI,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,MAAA;AACtC,EAAA,IAAI,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,MAAA;AACtC,EAAA,OAAO,MAAA;AACT;AASO,SAAS,eAAA,CAAgB,MAAc,QAAA,EAA2B;AAEvE,EAAA,MAAM,eAAe,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,MAAA,GAAS,IAAI,CAAC,CAAA;AAGlD,EAAA,OACE,YAAA,GAAe,YACf,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,IACxB,QAAA,CAAS,SAAS,MAAM,CAAA;AAE5B;AAKO,SAAS,oBAAoB,MAAA,EAA6B;AAC/D,EAAA,MAAM,YAAA,GAAe,KAAK,MAAM,CAAA;AAChC,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,YAAA,CAAa,MAAM,CAAA;AAChD,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,YAAA,CAAa,QAAQ,CAAA,EAAA,EAAK;AAC5C,IAAA,KAAA,CAAM,CAAC,CAAA,GAAI,YAAA,CAAa,UAAA,CAAW,CAAC,CAAA;AAAA,EACtC;AACA,EAAA,OAAO,KAAA,CAAM,MAAA;AACf;AAKO,SAAS,oBAAoB,MAAA,EAA6B;AAC/D,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAM,CAAA;AACnC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACxC;AACA,EAAA,OAAO,KAAK,MAAM,CAAA;AACpB","file":"image-processing.js","sourcesContent":["/**\n * Server-side image processing for Cloudflare Workers\n *\n * Uses @standardagents/sip for memory-efficient processing with scanline-based\n * resize and streaming output. Falls back to PNG encoding for transparent images.\n *\n * Features:\n * - Compress/resize images to fit under 1.5MB target\n * - Convert AVIF/WebP/PNG/JPEG to JPEG (default) or PNG (for transparency)\n * - Memory-efficient scanline processing\n * - Smart quality optimization: quality first, dimensions last\n */\n\nimport { sip, probe } from \"@standardagents/sip\";\nimport { encode as encodePng, decode as decodePng } from \"@jsquash/png\";\nimport { decode as decodeAvif } from \"@jsquash/avif\";\n\n// ImageData type for Workers (matches the structure from @jsquash)\ninterface ImageDataLike {\n data: Uint8ClampedArray;\n width: number;\n height: number;\n}\n\n// Target 1.5MB binary → ~2MB base64 for RPC transfer\n// SQLite limit is 2MB per row, but base64 overhead means we need headroom\nconst MAX_SIZE = 1.5 * 1024 * 1024;\n\n// Memory safety limit\nconst MAX_DIMENSION = 4096;\n\n// Reject images that would consume too much memory before decoding\nconst MAX_INPUT_SIZE = 20 * 1024 * 1024; // 20MB\n\nexport interface ProcessedImage {\n data: ArrayBuffer;\n mimeType: \"image/jpeg\" | \"image/png\";\n width: number;\n height: number;\n}\n\n/**\n * Process an image to ensure it's under 1.5MB and in a supported format.\n *\n * @param input - Raw image data as ArrayBuffer\n * @param inputMimeType - MIME type hint (used as fallback for format detection)\n * @returns Processed image data with updated mimeType and dimensions\n */\nexport async function processImage(\n input: ArrayBuffer,\n inputMimeType: string\n): Promise<ProcessedImage> {\n // Memory safety: reject very large images\n if (input.byteLength > MAX_INPUT_SIZE) {\n throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);\n }\n\n // Probe for format and transparency\n const probeResult = probe(input);\n const format = probeResult.format !== 'unknown' ? probeResult.format : detectFormat(input, inputMimeType);\n const hasAlpha = probeResult.hasAlpha;\n\n // For transparent images, we need to preserve alpha with PNG\n if (hasAlpha && (format === 'png' || format === 'avif' || format === 'webp')) {\n return await processPngWithAlpha(input, format);\n }\n\n // Use sip for efficient JPEG processing\n try {\n const result = await sip.process(input, {\n maxWidth: MAX_DIMENSION,\n maxHeight: MAX_DIMENSION,\n maxBytes: MAX_SIZE,\n quality: 85,\n });\n\n return {\n data: result.data,\n mimeType: \"image/jpeg\",\n width: result.width,\n height: result.height,\n };\n } catch (err) {\n // Fallback: if sip fails, try original processing\n console.error('[sip] Processing failed, falling back:', err);\n throw err;\n }\n}\n\n/**\n * Process PNG/AVIF images with transparency preservation\n */\nasync function processPngWithAlpha(\n input: ArrayBuffer,\n format: string\n): Promise<ProcessedImage> {\n // Decode based on format\n let imageData: ImageDataLike;\n\n if (format === 'avif') {\n imageData = await decodeAvif(input);\n } else {\n imageData = await decodePng(input);\n }\n\n const originalWidth = imageData.width;\n const originalHeight = imageData.height;\n\n // First try encoding at original size\n let encoded = await encodePng(imageData);\n\n if (encoded.byteLength <= MAX_SIZE) {\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: originalWidth,\n height: originalHeight,\n };\n }\n\n // PNG too large - resize using sip's scanline method and re-encode\n // We need to resize while preserving alpha, so we'll do a simple RGBA resize\n let scale = 0.9;\n while (encoded.byteLength > MAX_SIZE && scale > 0.15) {\n const newWidth = Math.floor(originalWidth * scale);\n const newHeight = Math.floor(originalHeight * scale);\n\n if (newWidth < 100 || newHeight < 100) {\n scale *= 0.9;\n continue;\n }\n\n // Simple bilinear resize for RGBA\n const resized = resizeRgba(\n new Uint8ClampedArray(imageData.data),\n originalWidth,\n originalHeight,\n newWidth,\n newHeight\n );\n\n const resizedImageData: ImageDataLike = { data: resized, width: newWidth, height: newHeight };\n encoded = await encodePng(resizedImageData);\n\n if (encoded.byteLength <= MAX_SIZE) {\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: newWidth,\n height: newHeight,\n };\n }\n\n scale *= 0.9;\n }\n\n // Fallback: smallest PNG\n const finalWidth = Math.floor(originalWidth * 0.15);\n const finalHeight = Math.floor(originalHeight * 0.15);\n const finalResized = resizeRgba(\n new Uint8ClampedArray(imageData.data),\n originalWidth,\n originalHeight,\n finalWidth,\n finalHeight\n );\n encoded = await encodePng({ data: finalResized, width: finalWidth, height: finalHeight });\n\n return {\n data: encoded,\n mimeType: \"image/png\",\n width: finalWidth,\n height: finalHeight,\n };\n}\n\n/**\n * Simple bilinear RGBA resize\n */\nfunction resizeRgba(\n src: Uint8ClampedArray,\n srcWidth: number,\n srcHeight: number,\n dstWidth: number,\n dstHeight: number\n): Uint8ClampedArray {\n const dst = new Uint8ClampedArray(dstWidth * dstHeight * 4);\n const xScale = srcWidth / dstWidth;\n const yScale = srcHeight / dstHeight;\n\n for (let dstY = 0; dstY < dstHeight; dstY++) {\n for (let dstX = 0; dstX < dstWidth; dstX++) {\n const srcXFloat = dstX * xScale;\n const srcYFloat = dstY * yScale;\n const srcX0 = Math.floor(srcXFloat);\n const srcY0 = Math.floor(srcYFloat);\n const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);\n const srcY1 = Math.min(srcY0 + 1, srcHeight - 1);\n const tx = srcXFloat - srcX0;\n const ty = srcYFloat - srcY0;\n\n const idx00 = (srcY0 * srcWidth + srcX0) * 4;\n const idx10 = (srcY0 * srcWidth + srcX1) * 4;\n const idx01 = (srcY1 * srcWidth + srcX0) * 4;\n const idx11 = (srcY1 * srcWidth + srcX1) * 4;\n const dstIdx = (dstY * dstWidth + dstX) * 4;\n\n for (let c = 0; c < 4; c++) {\n const v00 = src[idx00 + c];\n const v10 = src[idx10 + c];\n const v01 = src[idx01 + c];\n const v11 = src[idx11 + c];\n const top = v00 * (1 - tx) + v10 * tx;\n const bottom = v01 * (1 - tx) + v11 * tx;\n dst[dstIdx + c] = Math.round(top * (1 - ty) + bottom * ty);\n }\n }\n }\n\n return dst;\n}\n\n/**\n * Detect image format from magic bytes (fallback)\n */\nfunction detectFormat(data: ArrayBuffer, mimeType: string): string {\n const bytes = new Uint8Array(data.slice(0, 12));\n\n if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {\n const brand = String.fromCharCode(...bytes.slice(8, 12));\n if (brand === \"avif\" || brand === \"avis\") return \"avif\";\n }\n\n if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {\n return \"png\";\n }\n\n if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {\n return \"jpeg\";\n }\n\n if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&\n bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n return \"webp\";\n }\n\n if (mimeType.includes(\"png\")) return \"png\";\n if (mimeType.includes(\"webp\")) return \"webp\";\n if (mimeType.includes(\"avif\")) return \"avif\";\n return \"jpeg\";\n}\n\n/**\n * Check if an image needs processing based on size and format.\n *\n * @param data - Base64-encoded image data\n * @param mimeType - MIME type of the image\n * @returns true if processing is needed\n */\nexport function needsProcessing(data: string, mimeType: string): boolean {\n // Decode base64 to get actual size\n const binaryLength = Math.ceil(data.length * 3 / 4);\n\n // Process if >2MB or unsupported format\n return (\n binaryLength > MAX_SIZE ||\n mimeType.includes(\"avif\") ||\n mimeType.includes(\"webp\")\n );\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert ArrayBuffer to base64 string\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1186,6 +1186,8 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
1186
1186
  id: any;
1187
1187
  title: any;
1188
1188
  type: any;
1189
+ description: any;
1190
+ icon: any;
1189
1191
  side_a_label: any;
1190
1192
  side_b_label: any;
1191
1193
  } | null;
@@ -1358,6 +1360,22 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
1358
1360
  error: any;
1359
1361
  file?: undefined;
1360
1362
  }>;
1363
+ /**
1364
+ * Process an image using sip (WASM-based image processing)
1365
+ * This runs inside the DO where WASM is properly initialized.
1366
+ *
1367
+ * @param data - base64 encoded image data
1368
+ * @param mimeType - MIME type of the input image
1369
+ * @returns Processed image data, dimensions, and mimeType
1370
+ */
1371
+ processImage(data: string, mimeType: string): Promise<{
1372
+ success: boolean;
1373
+ data?: string;
1374
+ mimeType?: string;
1375
+ width?: number;
1376
+ height?: number;
1377
+ error?: string;
1378
+ }>;
1361
1379
  /**
1362
1380
  * Link to an external file (RPC method)
1363
1381
  */
@@ -2557,6 +2575,20 @@ interface AgentDefinition<N extends string = string> {
2557
2575
  * @example ['customer-service', 'tier-1', 'english']
2558
2576
  */
2559
2577
  tags?: string[];
2578
+ /**
2579
+ * Brief description of what this agent does.
2580
+ * Useful for UIs and documentation.
2581
+ *
2582
+ * @example 'Handles customer support inquiries and resolves issues'
2583
+ */
2584
+ description?: string;
2585
+ /**
2586
+ * Icon URL or absolute path for the agent.
2587
+ * Absolute paths (starting with `/`) are converted to full URLs in API responses.
2588
+ *
2589
+ * @example 'https://example.com/icon.svg' or '/icons/support.svg'
2590
+ */
2591
+ icon?: string;
2560
2592
  }
2561
2593
  /**
2562
2594
  * Defines an agent configuration.