lopata 0.0.1

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 (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,381 @@
1
+ // Images binding — Sharp-based implementation for local dev
2
+ // Supports resize, rotate, format conversion, quality, draw overlays, and AVIF dimensions.
3
+
4
+ import sharp from "sharp";
5
+
6
+ type ImageFormat = "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/avif" | "image/svg+xml";
7
+
8
+ export interface ImageInfo {
9
+ width: number;
10
+ height: number;
11
+ format: ImageFormat;
12
+ fileSize: number;
13
+ }
14
+
15
+ export interface ImageTransformOptions {
16
+ width?: number;
17
+ height?: number;
18
+ fit?: "contain" | "cover" | "crop" | "scale-down" | "pad";
19
+ rotate?: 0 | 90 | 180 | 270;
20
+ blur?: number;
21
+ brightness?: number;
22
+ contrast?: number;
23
+ sharpen?: number;
24
+ trim?: { top?: number; right?: number; bottom?: number; left?: number };
25
+ flip?: boolean;
26
+ flop?: boolean;
27
+ background?: string;
28
+ }
29
+
30
+ export interface DrawOptions {
31
+ top?: number;
32
+ left?: number;
33
+ bottom?: number;
34
+ right?: number;
35
+ opacity?: number;
36
+ repeat?: "repeat" | "no-repeat";
37
+ }
38
+
39
+ export interface OutputOptions {
40
+ format: "image/png" | "image/jpeg" | "image/webp" | "image/avif";
41
+ quality?: number;
42
+ }
43
+
44
+ export interface ImageOutputResult {
45
+ image(): ReadableStream<Uint8Array>;
46
+ contentType(): string;
47
+ }
48
+
49
+ // --- PNG header parsing ---
50
+
51
+ function parsePngSize(buf: Uint8Array): { width: number; height: number } | null {
52
+ if (buf.length < 24) return null;
53
+ if (buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47) return null;
54
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
55
+ return { width: view.getUint32(16), height: view.getUint32(20) };
56
+ }
57
+
58
+ // --- JPEG header parsing ---
59
+
60
+ function parseJpegSize(buf: Uint8Array): { width: number; height: number } | null {
61
+ if (buf.length < 2 || buf[0] !== 0xff || buf[1] !== 0xd8) return null;
62
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
63
+ let offset = 2;
64
+ while (offset + 4 < buf.length) {
65
+ if (buf[offset] !== 0xff) break;
66
+ const marker = buf[offset + 1]!;
67
+ if (
68
+ (marker >= 0xc0 && marker <= 0xc3) ||
69
+ (marker >= 0xc5 && marker <= 0xc7) ||
70
+ (marker >= 0xc9 && marker <= 0xcb) ||
71
+ (marker >= 0xcd && marker <= 0xcf)
72
+ ) {
73
+ if (offset + 9 > buf.length) return null;
74
+ const height = view.getUint16(offset + 5);
75
+ const width = view.getUint16(offset + 7);
76
+ return { width, height };
77
+ }
78
+ const segLen = view.getUint16(offset + 2);
79
+ offset += 2 + segLen;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // --- GIF header parsing ---
85
+
86
+ function parseGifSize(buf: Uint8Array): { width: number; height: number } | null {
87
+ if (buf.length < 10) return null;
88
+ if (buf[0] !== 0x47 || buf[1] !== 0x49 || buf[2] !== 0x46) return null;
89
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
90
+ return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
91
+ }
92
+
93
+ // --- WebP header parsing ---
94
+
95
+ function parseWebpSize(buf: Uint8Array): { width: number; height: number } | null {
96
+ if (buf.length < 30) return null;
97
+ if (buf[0] !== 0x52 || buf[1] !== 0x49 || buf[2] !== 0x46 || buf[3] !== 0x46) return null;
98
+ if (buf[8] !== 0x57 || buf[9] !== 0x45 || buf[10] !== 0x42 || buf[11] !== 0x50) return null;
99
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
100
+ if (buf[12] === 0x56 && buf[13] === 0x50 && buf[14] === 0x38 && buf[15] === 0x20) {
101
+ if (buf.length < 30) return null;
102
+ const width = view.getUint16(26, true) & 0x3fff;
103
+ const height = view.getUint16(28, true) & 0x3fff;
104
+ return { width, height };
105
+ }
106
+ if (buf[12] === 0x56 && buf[13] === 0x50 && buf[14] === 0x38 && buf[15] === 0x4c) {
107
+ if (buf.length < 25) return null;
108
+ const b0 = buf[21]!;
109
+ const b1 = buf[22]!;
110
+ const b2 = buf[23]!;
111
+ const b3 = buf[24]!;
112
+ const width = 1 + (((b1 & 0x3f) << 8) | b0);
113
+ const height = 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 >> 6) & 0x03));
114
+ return { width, height };
115
+ }
116
+ if (buf[12] === 0x56 && buf[13] === 0x50 && buf[14] === 0x38 && buf[15] === 0x58) {
117
+ if (buf.length < 30) return null;
118
+ const width = 1 + (buf[24]! | (buf[25]! << 8) | (buf[26]! << 16));
119
+ const height = 1 + (buf[27]! | (buf[28]! << 8) | (buf[29]! << 16));
120
+ return { width, height };
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // --- SVG parsing ---
126
+
127
+ function parseSvgSize(text: string): { width: number; height: number } | null {
128
+ const svgMatch = text.match(/<svg[^>]*>/i);
129
+ if (!svgMatch) return null;
130
+ const tag = svgMatch[0];
131
+ const wMatch = tag.match(/\bwidth\s*=\s*"(\d+)(?:px)?"/);
132
+ const hMatch = tag.match(/\bheight\s*=\s*"(\d+)(?:px)?"/);
133
+ if (wMatch?.[1] && hMatch?.[1]) {
134
+ return { width: parseInt(wMatch[1], 10), height: parseInt(hMatch[1], 10) };
135
+ }
136
+ const vbMatch = tag.match(/\bviewBox\s*=\s*"([^"]+)"/);
137
+ if (vbMatch?.[1]) {
138
+ const parts = vbMatch[1].trim().split(/[\s,]+/);
139
+ if (parts.length >= 4) {
140
+ return { width: Math.round(parseFloat(parts[2]!)), height: Math.round(parseFloat(parts[3]!)) };
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ // --- AVIF/HEIF container dimension parsing ---
147
+
148
+ function parseAvifSize(buf: Uint8Array): { width: number; height: number } | null {
149
+ // AVIF uses the ISOBMFF (ISO Base Media File Format) container.
150
+ // We look for the 'ispe' (ImageSpatialExtentsProperty) box which contains width/height.
151
+ // Format: 4-byte size, 4-byte type, then for 'ispe': 4-byte version/flags, 4-byte width, 4-byte height
152
+ if (buf.length < 12) return null;
153
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
154
+
155
+ // Scan for 'ispe' box type (0x69 0x73 0x70 0x65)
156
+ for (let i = 0; i <= buf.length - 20; i++) {
157
+ if (buf[i + 4] === 0x69 && buf[i + 5] === 0x73 && buf[i + 6] === 0x70 && buf[i + 7] === 0x65) {
158
+ // Found 'ispe' box. offset i: size(4) type(4) version+flags(4) width(4) height(4)
159
+ if (i + 20 > buf.length) return null;
160
+ const width = view.getUint32(i + 12);
161
+ const height = view.getUint32(i + 16);
162
+ if (width > 0 && height > 0 && width < 65536 && height < 65536) {
163
+ return { width, height };
164
+ }
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ // --- Detect format from bytes ---
171
+
172
+ function detectFormat(buf: Uint8Array): ImageFormat | null {
173
+ if (buf.length < 4) return null;
174
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
175
+ if (buf[0] === 0xff && buf[1] === 0xd8) return "image/jpeg";
176
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return "image/gif";
177
+ if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf.length >= 12 && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) return "image/webp";
178
+ if (buf.length >= 12 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) {
179
+ const brand = String.fromCharCode(buf[8]!, buf[9]!, buf[10]!, buf[11]!);
180
+ if (brand === "avif" || brand === "avis") return "image/avif";
181
+ }
182
+ const start = new TextDecoder().decode(buf.subarray(0, Math.min(buf.length, 256)));
183
+ if (start.includes("<svg") || start.includes("<?xml")) return "image/svg+xml";
184
+ return null;
185
+ }
186
+
187
+ // --- Parse dimensions ---
188
+
189
+ function parseDimensions(buf: Uint8Array, format: ImageFormat): { width: number; height: number } | null {
190
+ switch (format) {
191
+ case "image/png":
192
+ return parsePngSize(buf);
193
+ case "image/jpeg":
194
+ return parseJpegSize(buf);
195
+ case "image/gif":
196
+ return parseGifSize(buf);
197
+ case "image/webp":
198
+ return parseWebpSize(buf);
199
+ case "image/svg+xml":
200
+ return parseSvgSize(new TextDecoder().decode(buf));
201
+ case "image/avif":
202
+ return parseAvifSize(buf);
203
+ default:
204
+ return null;
205
+ }
206
+ }
207
+
208
+ async function readStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
209
+ const reader = stream.getReader();
210
+ const chunks: Uint8Array[] = [];
211
+ let totalLength = 0;
212
+ while (true) {
213
+ const { done, value } = await reader.read();
214
+ if (done) break;
215
+ chunks.push(value);
216
+ totalLength += value.byteLength;
217
+ }
218
+ const result = new Uint8Array(totalLength);
219
+ let offset = 0;
220
+ for (const chunk of chunks) {
221
+ result.set(chunk, offset);
222
+ offset += chunk.byteLength;
223
+ }
224
+ return result;
225
+ }
226
+
227
+ // --- Sharp format mapping ---
228
+
229
+ const MIME_TO_SHARP: Record<string, "png" | "jpeg" | "webp" | "avif"> = {
230
+ "image/png": "png",
231
+ "image/jpeg": "jpeg",
232
+ "image/webp": "webp",
233
+ "image/avif": "avif",
234
+ };
235
+
236
+ const CF_FIT_TO_SHARP: Record<string, "contain" | "cover" | "fill" | "inside" | "outside"> = {
237
+ contain: "contain",
238
+ cover: "cover",
239
+ crop: "cover",
240
+ "scale-down": "inside",
241
+ pad: "contain",
242
+ };
243
+
244
+ // --- LazyImageTransformer: Sharp-based ---
245
+
246
+ class LazyImageTransformer {
247
+ private streamPromise: Promise<Uint8Array>;
248
+ private transforms: ImageTransformOptions[] = [];
249
+ private overlays: { streamPromise: Promise<Uint8Array>; options?: DrawOptions }[] = [];
250
+
251
+ constructor(stream: ReadableStream<Uint8Array>) {
252
+ this.streamPromise = readStream(stream);
253
+ }
254
+
255
+ transform(options: ImageTransformOptions): LazyImageTransformer {
256
+ this.transforms.push(options);
257
+ return this;
258
+ }
259
+
260
+ draw(image: ReadableStream<Uint8Array>, options?: DrawOptions): LazyImageTransformer {
261
+ this.overlays.push({ streamPromise: readStream(image), options });
262
+ return this;
263
+ }
264
+
265
+ async output(options: OutputOptions): Promise<ImageOutputResult> {
266
+ let currentBuf = Buffer.from(await this.streamPromise);
267
+
268
+ // Apply each transform as a separate Sharp pipeline to ensure correct ordering
269
+ // (Sharp internally reorders operations within a single pipeline)
270
+ for (const t of this.transforms) {
271
+ let pipeline = sharp(currentBuf);
272
+
273
+ if (t.rotate !== undefined && t.rotate !== 0) {
274
+ pipeline = pipeline.rotate(t.rotate);
275
+ }
276
+ if (t.flip) {
277
+ pipeline = pipeline.flip();
278
+ }
279
+ if (t.flop) {
280
+ pipeline = pipeline.flop();
281
+ }
282
+ if (t.width !== undefined || t.height !== undefined) {
283
+ const fitVal = t.fit ? CF_FIT_TO_SHARP[t.fit] ?? "cover" : "cover";
284
+ const resizeOpts: sharp.ResizeOptions = { fit: fitVal };
285
+ if (t.background) resizeOpts.background = t.background;
286
+ pipeline = pipeline.resize(t.width ?? null, t.height ?? null, resizeOpts);
287
+ }
288
+ if (t.blur !== undefined && t.blur > 0) {
289
+ pipeline = pipeline.blur(Math.max(t.blur, 0.3));
290
+ }
291
+ if (t.sharpen !== undefined && t.sharpen > 0) {
292
+ pipeline = pipeline.sharpen(t.sharpen);
293
+ }
294
+ if (t.brightness !== undefined && t.brightness !== 1) {
295
+ pipeline = pipeline.modulate({ brightness: t.brightness });
296
+ }
297
+
298
+ currentBuf = Buffer.from(await pipeline.toBuffer());
299
+ }
300
+
301
+ // Apply draw overlays
302
+ if (this.overlays.length > 0) {
303
+ const composites: sharp.OverlayOptions[] = [];
304
+ for (const overlay of this.overlays) {
305
+ const overlayData = await overlay.streamPromise;
306
+ const opts: sharp.OverlayOptions = { input: Buffer.from(overlayData) };
307
+ if (overlay.options?.top !== undefined) opts.top = overlay.options.top;
308
+ if (overlay.options?.left !== undefined) opts.left = overlay.options.left;
309
+ if (overlay.options?.bottom !== undefined && overlay.options?.top === undefined) {
310
+ opts.gravity = "south";
311
+ }
312
+ if (overlay.options?.right !== undefined && overlay.options?.left === undefined) {
313
+ opts.gravity = "east";
314
+ }
315
+ if (overlay.options?.repeat === "repeat") {
316
+ opts.tile = true;
317
+ }
318
+ composites.push(opts);
319
+ }
320
+ currentBuf = Buffer.from(await sharp(currentBuf).composite(composites).toBuffer());
321
+ }
322
+
323
+ // Output format
324
+ const sharpFmt = MIME_TO_SHARP[options.format] ?? "png";
325
+ const formatOpts: Record<string, unknown> = {};
326
+ if (options.quality !== undefined) {
327
+ formatOpts.quality = options.quality;
328
+ }
329
+ const outputBuf = await sharp(currentBuf).toFormat(sharpFmt, formatOpts).toBuffer();
330
+ const contentType = options.format;
331
+
332
+ return {
333
+ image(): ReadableStream<Uint8Array> {
334
+ return new ReadableStream({
335
+ start(controller) {
336
+ controller.enqueue(new Uint8Array(outputBuf));
337
+ controller.close();
338
+ },
339
+ });
340
+ },
341
+ contentType(): string {
342
+ return contentType;
343
+ },
344
+ };
345
+ }
346
+ }
347
+
348
+ // --- ImagesBinding ---
349
+
350
+ export class ImagesBinding {
351
+ async info(stream: ReadableStream<Uint8Array>): Promise<ImageInfo> {
352
+ const buf = await readStream(stream);
353
+ const format = detectFormat(buf);
354
+ if (!format) {
355
+ throw new Error("Unsupported or unrecognizable image format");
356
+ }
357
+ // Try our fast header parsers first, fall back to Sharp for AVIF
358
+ let dims = parseDimensions(buf, format);
359
+ if (!dims && format === "image/avif") {
360
+ // Fallback: use Sharp metadata for AVIF
361
+ try {
362
+ const meta = await sharp(Buffer.from(buf)).metadata();
363
+ if (meta.width && meta.height) {
364
+ dims = { width: meta.width, height: meta.height };
365
+ }
366
+ } catch {
367
+ // ignore — return 0,0
368
+ }
369
+ }
370
+ return {
371
+ width: dims?.width ?? 0,
372
+ height: dims?.height ?? 0,
373
+ format,
374
+ fileSize: buf.byteLength,
375
+ };
376
+ }
377
+
378
+ input(stream: ReadableStream<Uint8Array>): LazyImageTransformer {
379
+ return new LazyImageTransformer(stream);
380
+ }
381
+ }