@flightdev/image 0.0.2

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Flight Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @flight-framework/image
2
+
3
+ Image optimization package for Flight Framework with Sharp, Squoosh, and CDN adapters.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @flight-framework/image
9
+
10
+ # Install your preferred processor:
11
+ npm install sharp # For Node.js (fastest)
12
+ # OR use CDN adapters (no additional install needed)
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { createImage } from '@flight-framework/image';
19
+ import { sharp } from '@flight-framework/image/sharp';
20
+
21
+ const image = createImage(sharp());
22
+
23
+ // Optimize an image
24
+ const result = await image.optimize('./hero.jpg', {
25
+ width: 800,
26
+ format: 'webp',
27
+ quality: 80,
28
+ });
29
+
30
+ // Generate blur placeholder
31
+ const blur = await image.blur('./hero.jpg');
32
+
33
+ // Generate responsive variants
34
+ const responsive = await image.responsive('./hero.jpg', {
35
+ widths: [640, 768, 1024, 1280],
36
+ formats: ['webp', 'avif'],
37
+ });
38
+ ```
39
+
40
+ ## Adapters
41
+
42
+ ### Sharp (Node.js)
43
+
44
+ Fastest option for Node.js environments.
45
+
46
+ ```typescript
47
+ import { sharp } from '@flight-framework/image/sharp';
48
+
49
+ const adapter = sharp({
50
+ cache: true, // Enable internal cache
51
+ concurrency: 4, // Parallel processing
52
+ simd: true, // SIMD optimization
53
+ });
54
+ ```
55
+
56
+ ### Squoosh (Edge/WASM)
57
+
58
+ Works on Edge runtimes (Cloudflare Workers, Vercel Edge).
59
+
60
+ ```typescript
61
+ import { squoosh } from '@flight-framework/image/squoosh';
62
+
63
+ const adapter = squoosh();
64
+ ```
65
+
66
+ ### Cloudinary (CDN)
67
+
68
+ CDN-based optimization, no local processing.
69
+
70
+ ```typescript
71
+ import { cloudinary } from '@flight-framework/image/cloudinary';
72
+
73
+ const adapter = cloudinary({
74
+ cloudName: 'my-cloud',
75
+ apiKey: process.env.CLOUDINARY_API_KEY,
76
+ apiSecret: process.env.CLOUDINARY_API_SECRET,
77
+ });
78
+ ```
79
+
80
+ ### Imgix (CDN)
81
+
82
+ ```typescript
83
+ import { imgix } from '@flight-framework/image/imgix';
84
+
85
+ const adapter = imgix({
86
+ domain: 'my-source.imgix.net',
87
+ secureUrlToken: process.env.IMGIX_TOKEN,
88
+ });
89
+ ```
90
+
91
+ ## UI Components
92
+
93
+ ### React
94
+
95
+ ```tsx
96
+ import { Image } from '@flight-framework/image/react';
97
+
98
+ <Image
99
+ src="/hero.jpg"
100
+ alt="Hero image"
101
+ width={800}
102
+ height={600}
103
+ priority // Preload for LCP
104
+ placeholder="blur" // Show blur while loading
105
+ blurDataUrl={blur} // From image.blur()
106
+ />
107
+ ```
108
+
109
+ ### Vue
110
+
111
+ ```vue
112
+ <script setup>
113
+ import { FlightImage } from '@flight-framework/image/vue';
114
+ </script>
115
+
116
+ <template>
117
+ <FlightImage
118
+ src="/hero.jpg"
119
+ alt="Hero"
120
+ :width="800"
121
+ :height="600"
122
+ priority
123
+ placeholder="blur"
124
+ />
125
+ </template>
126
+ ```
127
+
128
+ ### Svelte
129
+
130
+ ```svelte
131
+ <script>
132
+ import { createImageProps, lazyImage } from '@flight-framework/image/svelte';
133
+
134
+ const props = createImageProps({
135
+ src: '/hero.jpg',
136
+ alt: 'Hero',
137
+ width: 800,
138
+ height: 600,
139
+ });
140
+ </script>
141
+
142
+ <img {...props} use:lazyImage={blurDataUrl} />
143
+ ```
144
+
145
+ ### Solid
146
+
147
+ ```tsx
148
+ import { Image } from '@flight-framework/image/solid';
149
+
150
+ <Image
151
+ src="/hero.jpg"
152
+ alt="Hero"
153
+ width={800}
154
+ height={600}
155
+ priority
156
+ placeholder="blur"
157
+ />
158
+ ```
159
+
160
+ ## API Reference
161
+
162
+ ### ImageOptimizeOptions
163
+
164
+ | Option | Type | Default | Description |
165
+ |--------|------|---------|-------------|
166
+ | `width` | number | - | Target width |
167
+ | `height` | number | - | Target height |
168
+ | `format` | ImageFormat | 'auto' | Output format |
169
+ | `quality` | number | 80 | Quality (1-100) |
170
+ | `fit` | ImageFit | 'cover' | Resize fit mode |
171
+ | `blur` | boolean | false | Generate blur placeholder |
172
+ | `withoutEnlargement` | boolean | false | Prevent upscaling |
173
+ | `preserveMetadata` | boolean | false | Keep EXIF data |
174
+
175
+ ### ImageFormat
176
+
177
+ `'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'tiff' | 'auto'`
178
+
179
+ ### ImageFit
180
+
181
+ `'cover' | 'contain' | 'fill' | 'inside' | 'outside'`
182
+
183
+ ## Creating Custom Adapters
184
+
185
+ Implement the `ImageAdapter` interface:
186
+
187
+ ```typescript
188
+ import type { ImageAdapter } from '@flight-framework/image';
189
+
190
+ export function myAdapter(config: MyConfig): ImageAdapter {
191
+ return {
192
+ name: 'my-adapter',
193
+
194
+ supportsFormat(format) {
195
+ return ['webp', 'jpeg', 'png'].includes(format);
196
+ },
197
+
198
+ async getMetadata(input) {
199
+ // Return image dimensions, format, etc.
200
+ },
201
+
202
+ async optimize(input, options) {
203
+ // Process and return optimized image
204
+ },
205
+
206
+ async generateBlurPlaceholder(input, size) {
207
+ // Return base64 blur placeholder
208
+ },
209
+
210
+ async generateResponsive(input, config) {
211
+ // Generate multiple sizes
212
+ },
213
+ };
214
+ }
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,46 @@
1
+ import { ImageAdapterFactory } from '../index.js';
2
+ import 'node:buffer';
3
+
4
+ /**
5
+ * Cloudinary Adapter for @flightdev/image
6
+ *
7
+ * CDN-based image optimization using Cloudinary's transformation API.
8
+ * No local processing required - images are transformed at the CDN edge.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createImage } from '@flightdev/image';
13
+ * import { cloudinary } from '@flightdev/image/cloudinary';
14
+ *
15
+ * const image = createImage(cloudinary({
16
+ * cloudName: 'my-cloud',
17
+ * apiKey: process.env.CLOUDINARY_API_KEY,
18
+ * apiSecret: process.env.CLOUDINARY_API_SECRET,
19
+ * }));
20
+ * ```
21
+ */
22
+
23
+ interface CloudinaryConfig {
24
+ /** Cloudinary cloud name */
25
+ cloudName: string;
26
+ /** API key (optional for URL generation, required for uploads) */
27
+ apiKey?: string;
28
+ /** API secret (optional for URL generation, required for uploads) */
29
+ apiSecret?: string;
30
+ /** Use secure URLs (https) */
31
+ secure?: boolean;
32
+ /** Default folder for assets */
33
+ folder?: string;
34
+ }
35
+ /**
36
+ * Create a Cloudinary-based image adapter
37
+ *
38
+ * Cloudinary is a CDN-based image optimization service.
39
+ * Images are transformed at the edge, requiring no local processing.
40
+ *
41
+ * @param config - Cloudinary configuration
42
+ * @returns ImageAdapter instance
43
+ */
44
+ declare const cloudinary: ImageAdapterFactory<CloudinaryConfig>;
45
+
46
+ export { type CloudinaryConfig, cloudinary, cloudinary as default };
@@ -0,0 +1,160 @@
1
+ // src/adapters/cloudinary.ts
2
+ function buildCloudinaryUrl(cloudName, publicId, transformations, secure = true) {
3
+ const protocol = secure ? "https" : "http";
4
+ const transform = transformations.length > 0 ? transformations.join(",") + "/" : "";
5
+ return `${protocol}://res.cloudinary.com/${cloudName}/image/upload/${transform}${publicId}`;
6
+ }
7
+ function formatToCloudinary(format) {
8
+ if (format === "auto") return "f_auto";
9
+ return `f_${format}`;
10
+ }
11
+ function fitToCloudinary(fit) {
12
+ switch (fit) {
13
+ case "cover":
14
+ return "c_fill";
15
+ case "contain":
16
+ return "c_fit";
17
+ case "fill":
18
+ return "c_scale";
19
+ case "inside":
20
+ return "c_limit";
21
+ case "outside":
22
+ return "c_mfit";
23
+ default:
24
+ return "c_fill";
25
+ }
26
+ }
27
+ var cloudinary = (config) => {
28
+ if (!config?.cloudName) {
29
+ throw new Error("@flightdev/image: Cloudinary requires a cloudName configuration");
30
+ }
31
+ const { cloudName, secure = true, folder = "" } = config;
32
+ const adapter = {
33
+ name: "cloudinary",
34
+ supportsFormat(format) {
35
+ return ["jpeg", "png", "webp", "avif", "gif", "tiff", "auto"].includes(format);
36
+ },
37
+ async getMetadata(input) {
38
+ if (typeof input !== "string") {
39
+ throw new Error("Cloudinary getMetadata requires a URL or public ID");
40
+ }
41
+ return {
42
+ width: 0,
43
+ height: 0,
44
+ format: "unknown"
45
+ };
46
+ },
47
+ async optimize(input, options) {
48
+ if (typeof input !== "string") {
49
+ throw new Error(
50
+ "Cloudinary adapter works with URLs/public IDs.\nFor buffer processing, use Sharp or Squoosh adapter."
51
+ );
52
+ }
53
+ const transformations = [];
54
+ if (options.quality) {
55
+ transformations.push(`q_${options.quality}`);
56
+ } else {
57
+ transformations.push("q_auto");
58
+ }
59
+ if (options.format) {
60
+ transformations.push(formatToCloudinary(options.format));
61
+ } else {
62
+ transformations.push("f_auto");
63
+ }
64
+ if (options.width) {
65
+ transformations.push(`w_${options.width}`);
66
+ }
67
+ if (options.height) {
68
+ transformations.push(`h_${options.height}`);
69
+ }
70
+ if (options.fit) {
71
+ transformations.push(fitToCloudinary(options.fit));
72
+ }
73
+ if (options.withoutEnlargement) {
74
+ transformations.push("c_limit");
75
+ }
76
+ const publicId = folder ? `${folder}/${input}` : input;
77
+ const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);
78
+ const response = await fetch(url);
79
+ if (!response.ok) {
80
+ throw new Error(`Cloudinary request failed: ${response.status}`);
81
+ }
82
+ const buffer = Buffer.from(await response.arrayBuffer());
83
+ const contentType = response.headers.get("content-type") ?? "image/webp";
84
+ const format = contentType.replace("image/", "");
85
+ let blurDataUrl;
86
+ if (options.blur) {
87
+ blurDataUrl = await adapter.generateBlurPlaceholder(input);
88
+ }
89
+ return {
90
+ buffer,
91
+ format,
92
+ width: options.width ?? 0,
93
+ height: options.height ?? 0,
94
+ size: buffer.length,
95
+ blurDataUrl
96
+ };
97
+ },
98
+ async generateBlurPlaceholder(input, size = 10) {
99
+ if (typeof input !== "string") {
100
+ throw new Error("Cloudinary generateBlurPlaceholder requires a URL/public ID");
101
+ }
102
+ const transformations = [
103
+ `w_${size}`,
104
+ `h_${size}`,
105
+ "c_fill",
106
+ "e_blur:1000",
107
+ "q_20",
108
+ "f_webp"
109
+ ];
110
+ const publicId = folder ? `${folder}/${input}` : input;
111
+ const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);
112
+ const response = await fetch(url);
113
+ if (!response.ok) {
114
+ throw new Error(`Cloudinary blur request failed: ${response.status}`);
115
+ }
116
+ const buffer = Buffer.from(await response.arrayBuffer());
117
+ return `data:image/webp;base64,${buffer.toString("base64")}`;
118
+ },
119
+ async generateResponsive(input, config2) {
120
+ if (typeof input !== "string") {
121
+ throw new Error("Cloudinary generateResponsive requires a URL/public ID");
122
+ }
123
+ const formats = config2.formats ?? [config2.format ?? "webp"];
124
+ const variants = [];
125
+ for (const width of config2.widths) {
126
+ for (const format of formats) {
127
+ const transformations = [
128
+ `w_${width}`,
129
+ formatToCloudinary(format),
130
+ "q_auto"
131
+ ];
132
+ const publicId = folder ? `${folder}/${input}` : input;
133
+ const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);
134
+ variants.push({
135
+ width,
136
+ format,
137
+ url,
138
+ buffer: Buffer.alloc(0)
139
+ // CDN - no local buffer
140
+ });
141
+ }
142
+ }
143
+ const srcset = variants.filter((v) => v.format === formats[0]).map((v) => `${v.url} ${v.width}w`).join(", ");
144
+ const sizes = config2.sizes ?? "100vw";
145
+ const blurDataUrl = await adapter.generateBlurPlaceholder(input);
146
+ return {
147
+ srcset,
148
+ sizes,
149
+ variants,
150
+ blurDataUrl
151
+ };
152
+ }
153
+ };
154
+ return adapter;
155
+ };
156
+ var cloudinary_default = cloudinary;
157
+
158
+ export { cloudinary, cloudinary_default as default };
159
+ //# sourceMappingURL=cloudinary.js.map
160
+ //# sourceMappingURL=cloudinary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/cloudinary.ts"],"names":["config"],"mappings":"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","file":"cloudinary.js","sourcesContent":["/**\r\n * Cloudinary Adapter for @flightdev/image\r\n * \r\n * CDN-based image optimization using Cloudinary's transformation API.\r\n * No local processing required - images are transformed at the CDN edge.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createImage } from '@flightdev/image';\r\n * import { cloudinary } from '@flightdev/image/cloudinary';\r\n * \r\n * const image = createImage(cloudinary({\r\n * cloudName: 'my-cloud',\r\n * apiKey: process.env.CLOUDINARY_API_KEY,\r\n * apiSecret: process.env.CLOUDINARY_API_SECRET,\r\n * }));\r\n * ```\r\n */\r\n\r\nimport type {\r\n ImageAdapter,\r\n ImageAdapterFactory,\r\n ImageFormat,\r\n ImageMetadata,\r\n ImageOptimizeOptions,\r\n ImageOptimizeResult,\r\n ResponsiveConfig,\r\n ResponsiveResult,\r\n} from '../index.js';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport interface CloudinaryConfig {\r\n /** Cloudinary cloud name */\r\n cloudName: string;\r\n /** API key (optional for URL generation, required for uploads) */\r\n apiKey?: string;\r\n /** API secret (optional for URL generation, required for uploads) */\r\n apiSecret?: string;\r\n /** Use secure URLs (https) */\r\n secure?: boolean;\r\n /** Default folder for assets */\r\n folder?: string;\r\n}\r\n\r\n// ============================================================================\r\n// Cloudinary URL Builder\r\n// ============================================================================\r\n\r\nfunction buildCloudinaryUrl(\r\n cloudName: string,\r\n publicId: string,\r\n transformations: string[],\r\n secure = true\r\n): string {\r\n const protocol = secure ? 'https' : 'http';\r\n const transform = transformations.length > 0 ? transformations.join(',') + '/' : '';\r\n return `${protocol}://res.cloudinary.com/${cloudName}/image/upload/${transform}${publicId}`;\r\n}\r\n\r\nfunction formatToCloudinary(format: ImageFormat): string {\r\n if (format === 'auto') return 'f_auto';\r\n return `f_${format}`;\r\n}\r\n\r\nfunction fitToCloudinary(fit: string | undefined): string {\r\n switch (fit) {\r\n case 'cover': return 'c_fill';\r\n case 'contain': return 'c_fit';\r\n case 'fill': return 'c_scale';\r\n case 'inside': return 'c_limit';\r\n case 'outside': return 'c_mfit';\r\n default: return 'c_fill';\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Cloudinary Adapter Implementation\r\n// ============================================================================\r\n\r\n/**\r\n * Create a Cloudinary-based image adapter\r\n * \r\n * Cloudinary is a CDN-based image optimization service.\r\n * Images are transformed at the edge, requiring no local processing.\r\n * \r\n * @param config - Cloudinary configuration\r\n * @returns ImageAdapter instance\r\n */\r\nexport const cloudinary: ImageAdapterFactory<CloudinaryConfig> = (config) => {\r\n if (!config?.cloudName) {\r\n throw new Error('@flightdev/image: Cloudinary requires a cloudName configuration');\r\n }\r\n\r\n const { cloudName, secure = true, folder = '' } = config;\r\n\r\n const adapter: ImageAdapter = {\r\n name: 'cloudinary',\r\n\r\n supportsFormat(format: ImageFormat): boolean {\r\n // Cloudinary supports all common formats\r\n return ['jpeg', 'png', 'webp', 'avif', 'gif', 'tiff', 'auto'].includes(format);\r\n },\r\n\r\n async getMetadata(input: Buffer | string): Promise<ImageMetadata> {\r\n // For CDN adapters, metadata requires an API call\r\n // For now, return placeholder - in production you'd call Cloudinary API\r\n if (typeof input !== 'string') {\r\n throw new Error('Cloudinary getMetadata requires a URL or public ID');\r\n }\r\n\r\n // Extract from fl_getinfo would require API call\r\n return {\r\n width: 0,\r\n height: 0,\r\n format: 'unknown',\r\n };\r\n },\r\n\r\n async optimize(input: Buffer | string, options: ImageOptimizeOptions): Promise<ImageOptimizeResult> {\r\n if (typeof input !== 'string') {\r\n throw new Error(\r\n 'Cloudinary adapter works with URLs/public IDs.\\n' +\r\n 'For buffer processing, use Sharp or Squoosh adapter.'\r\n );\r\n }\r\n\r\n const transformations: string[] = [];\r\n\r\n // Quality\r\n if (options.quality) {\r\n transformations.push(`q_${options.quality}`);\r\n } else {\r\n transformations.push('q_auto');\r\n }\r\n\r\n // Format\r\n if (options.format) {\r\n transformations.push(formatToCloudinary(options.format));\r\n } else {\r\n transformations.push('f_auto');\r\n }\r\n\r\n // Resize\r\n if (options.width) {\r\n transformations.push(`w_${options.width}`);\r\n }\r\n if (options.height) {\r\n transformations.push(`h_${options.height}`);\r\n }\r\n\r\n // Fit\r\n if (options.fit) {\r\n transformations.push(fitToCloudinary(options.fit));\r\n }\r\n\r\n // Prevent enlargement\r\n if (options.withoutEnlargement) {\r\n transformations.push('c_limit');\r\n }\r\n\r\n const publicId = folder ? `${folder}/${input}` : input;\r\n const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);\r\n\r\n // Fetch the optimized image\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Cloudinary request failed: ${response.status}`);\r\n }\r\n\r\n const buffer = Buffer.from(await response.arrayBuffer());\r\n const contentType = response.headers.get('content-type') ?? 'image/webp';\r\n const format = contentType.replace('image/', '') as ImageFormat;\r\n\r\n // Generate blur if requested\r\n let blurDataUrl: string | undefined;\r\n if (options.blur) {\r\n blurDataUrl = await adapter.generateBlurPlaceholder(input);\r\n }\r\n\r\n return {\r\n buffer,\r\n format,\r\n width: options.width ?? 0,\r\n height: options.height ?? 0,\r\n size: buffer.length,\r\n blurDataUrl,\r\n };\r\n },\r\n\r\n async generateBlurPlaceholder(input: Buffer | string, size = 10): Promise<string> {\r\n if (typeof input !== 'string') {\r\n throw new Error('Cloudinary generateBlurPlaceholder requires a URL/public ID');\r\n }\r\n\r\n const transformations = [\r\n `w_${size}`,\r\n `h_${size}`,\r\n 'c_fill',\r\n 'e_blur:1000',\r\n 'q_20',\r\n 'f_webp',\r\n ];\r\n\r\n const publicId = folder ? `${folder}/${input}` : input;\r\n const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);\r\n\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Cloudinary blur request failed: ${response.status}`);\r\n }\r\n\r\n const buffer = Buffer.from(await response.arrayBuffer());\r\n return `data:image/webp;base64,${buffer.toString('base64')}`;\r\n },\r\n\r\n async generateResponsive(input: Buffer | string, config: ResponsiveConfig): Promise<ResponsiveResult> {\r\n if (typeof input !== 'string') {\r\n throw new Error('Cloudinary generateResponsive requires a URL/public ID');\r\n }\r\n\r\n const formats = config.formats ?? [config.format ?? 'webp'];\r\n const variants: ResponsiveResult['variants'] = [];\r\n\r\n for (const width of config.widths) {\r\n for (const format of formats) {\r\n const transformations = [\r\n `w_${width}`,\r\n formatToCloudinary(format),\r\n 'q_auto',\r\n ];\r\n\r\n const publicId = folder ? `${folder}/${input}` : input;\r\n const url = buildCloudinaryUrl(cloudName, publicId, transformations, secure);\r\n\r\n variants.push({\r\n width,\r\n format,\r\n url,\r\n buffer: Buffer.alloc(0), // CDN - no local buffer\r\n });\r\n }\r\n }\r\n\r\n const srcset = variants\r\n .filter(v => v.format === formats[0])\r\n .map(v => `${v.url} ${v.width}w`)\r\n .join(', ');\r\n\r\n const sizes = config.sizes ?? '100vw';\r\n const blurDataUrl = await adapter.generateBlurPlaceholder(input);\r\n\r\n return {\r\n srcset,\r\n sizes,\r\n variants,\r\n blurDataUrl,\r\n };\r\n },\r\n };\r\n\r\n return adapter;\r\n};\r\n\r\nexport default cloudinary;\r\n"]}
@@ -0,0 +1,39 @@
1
+ import { ImageAdapterFactory } from '../index.js';
2
+ import 'node:buffer';
3
+
4
+ /**
5
+ * Imgix Adapter for @flightdev/image
6
+ *
7
+ * CDN-based image optimization using Imgix's transformation API.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createImage } from '@flightdev/image';
12
+ * import { imgix } from '@flightdev/image/imgix';
13
+ *
14
+ * const image = createImage(imgix({
15
+ * domain: 'my-source.imgix.net',
16
+ * secureUrlToken: process.env.IMGIX_TOKEN, // optional
17
+ * }));
18
+ * ```
19
+ */
20
+
21
+ interface ImgixConfig {
22
+ /** Imgix source domain (e.g., 'my-source.imgix.net') */
23
+ domain: string;
24
+ /** Secure URL token for signed URLs (optional) */
25
+ secureUrlToken?: string;
26
+ /** Use HTTPS */
27
+ useHttps?: boolean;
28
+ }
29
+ /**
30
+ * Create an Imgix-based image adapter
31
+ *
32
+ * Imgix is a CDN-based image optimization service.
33
+ *
34
+ * @param config - Imgix configuration
35
+ * @returns ImageAdapter instance
36
+ */
37
+ declare const imgix: ImageAdapterFactory<ImgixConfig>;
38
+
39
+ export { type ImgixConfig, imgix as default, imgix };
@@ -0,0 +1,152 @@
1
+ // src/adapters/imgix.ts
2
+ function buildImgixUrl(domain, path, params, useHttps = true) {
3
+ const protocol = useHttps ? "https" : "http";
4
+ const query = new URLSearchParams();
5
+ for (const [key, value] of Object.entries(params)) {
6
+ query.set(key, String(value));
7
+ }
8
+ const queryString = query.toString();
9
+ return `${protocol}://${domain}${path}${queryString ? "?" + queryString : ""}`;
10
+ }
11
+ function formatToImgix(format) {
12
+ return format === "auto" ? "auto" : format;
13
+ }
14
+ function fitToImgix(fit) {
15
+ switch (fit) {
16
+ case "cover":
17
+ return "crop";
18
+ case "contain":
19
+ return "fit";
20
+ case "fill":
21
+ return "scale";
22
+ case "inside":
23
+ return "max";
24
+ case "outside":
25
+ return "min";
26
+ default:
27
+ return "crop";
28
+ }
29
+ }
30
+ var imgix = (config) => {
31
+ if (!config?.domain) {
32
+ throw new Error("@flightdev/image: Imgix requires a domain configuration");
33
+ }
34
+ const { domain, useHttps = true } = config;
35
+ const adapter = {
36
+ name: "imgix",
37
+ supportsFormat(format) {
38
+ return ["jpeg", "png", "webp", "avif", "gif", "auto"].includes(format);
39
+ },
40
+ async getMetadata(input) {
41
+ if (typeof input !== "string") {
42
+ throw new Error("Imgix getMetadata requires a path");
43
+ }
44
+ const url = buildImgixUrl(domain, input, { fm: "json" }, useHttps);
45
+ try {
46
+ const response = await fetch(url);
47
+ if (!response.ok) {
48
+ throw new Error(`Imgix metadata request failed: ${response.status}`);
49
+ }
50
+ const data = await response.json();
51
+ return {
52
+ width: data.PixelWidth ?? 0,
53
+ height: data.PixelHeight ?? 0,
54
+ format: data["Content-Type"]?.replace("image/", "") ?? "unknown",
55
+ size: parseInt(data["Content-Length"] ?? "0", 10)
56
+ };
57
+ } catch {
58
+ return { width: 0, height: 0, format: "unknown" };
59
+ }
60
+ },
61
+ async optimize(input, options) {
62
+ if (typeof input !== "string") {
63
+ throw new Error("Imgix adapter works with paths/URLs only");
64
+ }
65
+ const params = {
66
+ auto: "format,compress"
67
+ };
68
+ if (options.width) params.w = options.width;
69
+ if (options.height) params.h = options.height;
70
+ if (options.quality) params.q = options.quality;
71
+ if (options.format && options.format !== "auto") params.fm = formatToImgix(options.format);
72
+ if (options.fit) params.fit = fitToImgix(options.fit);
73
+ const url = buildImgixUrl(domain, input, params, useHttps);
74
+ const response = await fetch(url);
75
+ if (!response.ok) {
76
+ throw new Error(`Imgix request failed: ${response.status}`);
77
+ }
78
+ const buffer = Buffer.from(await response.arrayBuffer());
79
+ const contentType = response.headers.get("content-type") ?? "image/webp";
80
+ const format = contentType.replace("image/", "");
81
+ let blurDataUrl;
82
+ if (options.blur) {
83
+ blurDataUrl = await adapter.generateBlurPlaceholder(input);
84
+ }
85
+ return {
86
+ buffer,
87
+ format,
88
+ width: options.width ?? 0,
89
+ height: options.height ?? 0,
90
+ size: buffer.length,
91
+ blurDataUrl
92
+ };
93
+ },
94
+ async generateBlurPlaceholder(input, size = 10) {
95
+ if (typeof input !== "string") {
96
+ throw new Error("Imgix generateBlurPlaceholder requires a path");
97
+ }
98
+ const params = {
99
+ w: size,
100
+ h: size,
101
+ blur: 200,
102
+ q: 20,
103
+ fm: "webp"
104
+ };
105
+ const url = buildImgixUrl(domain, input, params, useHttps);
106
+ const response = await fetch(url);
107
+ if (!response.ok) {
108
+ throw new Error(`Imgix blur request failed: ${response.status}`);
109
+ }
110
+ const buffer = Buffer.from(await response.arrayBuffer());
111
+ return `data:image/webp;base64,${buffer.toString("base64")}`;
112
+ },
113
+ async generateResponsive(input, config2) {
114
+ if (typeof input !== "string") {
115
+ throw new Error("Imgix generateResponsive requires a path");
116
+ }
117
+ const formats = config2.formats ?? [config2.format ?? "webp"];
118
+ const variants = [];
119
+ for (const width of config2.widths) {
120
+ for (const format of formats) {
121
+ const params = {
122
+ w: width,
123
+ auto: "compress"
124
+ };
125
+ if (format !== "auto") params.fm = format;
126
+ const url = buildImgixUrl(domain, input, params, useHttps);
127
+ variants.push({
128
+ width,
129
+ format,
130
+ url,
131
+ buffer: Buffer.alloc(0)
132
+ });
133
+ }
134
+ }
135
+ const srcset = variants.filter((v) => v.format === formats[0]).map((v) => `${v.url} ${v.width}w`).join(", ");
136
+ const sizes = config2.sizes ?? "100vw";
137
+ const blurDataUrl = await adapter.generateBlurPlaceholder(input);
138
+ return {
139
+ srcset,
140
+ sizes,
141
+ variants,
142
+ blurDataUrl
143
+ };
144
+ }
145
+ };
146
+ return adapter;
147
+ };
148
+ var imgix_default = imgix;
149
+
150
+ export { imgix_default as default, imgix };
151
+ //# sourceMappingURL=imgix.js.map
152
+ //# sourceMappingURL=imgix.js.map