@standardagents/builder 0.9.17 → 0.10.1-dev.114898

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Server-side image processing for Cloudflare Workers
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
+ *
7
+ * Features:
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
11
+ * - Smart quality optimization: quality first, dimensions last
12
+ */
13
+ interface ProcessedImage {
14
+ data: ArrayBuffer;
15
+ mimeType: "image/jpeg" | "image/png";
16
+ width: number;
17
+ height: number;
18
+ }
19
+ /**
20
+ * Process an image to ensure it's under 1.5MB and in a supported format.
21
+ *
22
+ * @param input - Raw image data as ArrayBuffer
23
+ * @param inputMimeType - MIME type hint (used as fallback for format detection)
24
+ * @returns Processed image data with updated mimeType and dimensions
25
+ */
26
+ declare function processImage(input: ArrayBuffer, inputMimeType: string): Promise<ProcessedImage>;
27
+ /**
28
+ * Check if an image needs processing based on size and format.
29
+ *
30
+ * @param data - Base64-encoded image data
31
+ * @param mimeType - MIME type of the image
32
+ * @returns true if processing is needed
33
+ */
34
+ declare function needsProcessing(data: string, mimeType: string): boolean;
35
+ /**
36
+ * Convert base64 string to ArrayBuffer
37
+ */
38
+ declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
39
+ /**
40
+ * Convert ArrayBuffer to base64 string
41
+ */
42
+ declare function arrayBufferToBase64(buffer: ArrayBuffer): string;
43
+
44
+ export { type ProcessedImage, arrayBufferToBase64, base64ToArrayBuffer, needsProcessing, processImage };
@@ -0,0 +1,174 @@
1
+ import { probe, sip } from '@standardagents/sip';
2
+ import { decode as decode$1, encode } from '@jsquash/png';
3
+ import { decode } from '@jsquash/avif';
4
+
5
+ // src/image-processing/index.ts
6
+ var MAX_SIZE = 1.5 * 1024 * 1024;
7
+ var MAX_DIMENSION = 4096;
8
+ var MAX_INPUT_SIZE = 20 * 1024 * 1024;
9
+ async function processImage(input, inputMimeType) {
10
+ if (input.byteLength > MAX_INPUT_SIZE) {
11
+ throw new Error(`Image too large: ${input.byteLength} bytes exceeds ${MAX_INPUT_SIZE} byte limit`);
12
+ }
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);
18
+ }
19
+ try {
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,
28
+ mimeType: "image/jpeg",
29
+ width: result.width,
30
+ height: result.height
31
+ };
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);
43
+ }
44
+ const originalWidth = imageData.width;
45
+ const originalHeight = imageData.height;
46
+ let encoded = await encode(imageData);
47
+ if (encoded.byteLength <= MAX_SIZE) {
48
+ return {
49
+ data: encoded,
50
+ mimeType: "image/png",
51
+ width: originalWidth,
52
+ height: originalHeight
53
+ };
54
+ }
55
+ let scale = 0.9;
56
+ while (encoded.byteLength > MAX_SIZE && scale > 0.15) {
57
+ const newWidth = Math.floor(originalWidth * scale);
58
+ const newHeight = Math.floor(originalHeight * scale);
59
+ if (newWidth < 100 || newHeight < 100) {
60
+ scale *= 0.9;
61
+ continue;
62
+ }
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 };
71
+ encoded = await encode(resizedImageData);
72
+ if (encoded.byteLength <= MAX_SIZE) {
73
+ return {
74
+ data: encoded,
75
+ mimeType: "image/png",
76
+ width: newWidth,
77
+ height: newHeight
78
+ };
79
+ }
80
+ scale *= 0.9;
81
+ }
82
+ const finalWidth = Math.floor(originalWidth * 0.15);
83
+ const finalHeight = Math.floor(originalHeight * 0.15);
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 });
92
+ return {
93
+ data: encoded,
94
+ mimeType: "image/png",
95
+ width: finalWidth,
96
+ height: finalHeight
97
+ };
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
+ }
151
+ function needsProcessing(data, mimeType) {
152
+ const binaryLength = Math.ceil(data.length * 3 / 4);
153
+ return binaryLength > MAX_SIZE || mimeType.includes("avif") || mimeType.includes("webp");
154
+ }
155
+ function base64ToArrayBuffer(base64) {
156
+ const binaryString = atob(base64);
157
+ const bytes = new Uint8Array(binaryString.length);
158
+ for (let i = 0; i < binaryString.length; i++) {
159
+ bytes[i] = binaryString.charCodeAt(i);
160
+ }
161
+ return bytes.buffer;
162
+ }
163
+ function arrayBufferToBase64(buffer) {
164
+ const bytes = new Uint8Array(buffer);
165
+ let binary = "";
166
+ for (let i = 0; i < bytes.length; i++) {
167
+ binary += String.fromCharCode(bytes[i]);
168
+ }
169
+ return btoa(binary);
170
+ }
171
+
172
+ export { arrayBufferToBase64, base64ToArrayBuffer, needsProcessing, processImage };
173
+ //# sourceMappingURL=image-processing.js.map
174
+ //# sourceMappingURL=image-processing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/image-processing/index.ts"],"names":["decodeAvif","decodePng","encodePng"],"mappings":"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","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"]}