@happyvertical/images 0.80.0 → 0.80.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.
package/dist/index.js CHANGED
@@ -1,1302 +1,1497 @@
1
1
  import { createHmac } from "node:crypto";
2
- class ImageError extends Error {
3
- constructor(message, code, adapter) {
4
- super(message);
5
- this.code = code;
6
- this.adapter = adapter;
7
- this.name = "ImageError";
8
- }
9
- }
10
- class ImageNotFoundError extends ImageError {
11
- constructor(path, adapter) {
12
- super(`Image not found: ${path}`, "IMAGE_NOT_FOUND", adapter);
13
- this.name = "ImageNotFoundError";
14
- }
15
- }
16
- class UnsupportedFormatError extends ImageError {
17
- constructor(format, adapter) {
18
- super(`Unsupported image format: ${format}`, "UNSUPPORTED_FORMAT", adapter);
19
- this.name = "UnsupportedFormatError";
20
- }
21
- }
22
- class OperationNotSupportedError extends ImageError {
23
- constructor(operation, adapter) {
24
- super(
25
- `Operation not supported: ${operation}`,
26
- "OPERATION_NOT_SUPPORTED",
27
- adapter
28
- );
29
- this.name = "OperationNotSupportedError";
30
- }
31
- }
32
- class ProcessingError extends ImageError {
33
- constructor(message, adapter, cause) {
34
- super(message, "PROCESSING_ERROR", adapter);
35
- this.cause = cause;
36
- this.name = "ProcessingError";
37
- }
38
- }
39
- class InvalidAdapterError extends ImageError {
40
- constructor(type) {
41
- super(`Invalid adapter type: ${type}`, "INVALID_ADAPTER");
42
- this.name = "InvalidAdapterError";
43
- }
44
- }
45
- class RemoteServiceError extends ImageError {
46
- constructor(message, adapter, statusCode) {
47
- super(message, "REMOTE_SERVICE_ERROR", adapter);
48
- this.statusCode = statusCode;
49
- this.name = "RemoteServiceError";
50
- }
51
- }
2
+ //#region \0rolldown/runtime.js
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
+ return target;
12
+ };
13
+ //#endregion
14
+ //#region src/shared/errors.ts
15
+ /**
16
+ * Error classes for the images library
17
+ */
18
+ /**
19
+ * Base error class for image processing operations
20
+ */
21
+ var ImageError = class extends Error {
22
+ code;
23
+ adapter;
24
+ constructor(message, code, adapter) {
25
+ super(message);
26
+ this.code = code;
27
+ this.adapter = adapter;
28
+ this.name = "ImageError";
29
+ }
30
+ };
31
+ /**
32
+ * Error thrown when an image file is not found
33
+ */
34
+ var ImageNotFoundError = class extends ImageError {
35
+ constructor(path, adapter) {
36
+ super(`Image not found: ${path}`, "IMAGE_NOT_FOUND", adapter);
37
+ this.name = "ImageNotFoundError";
38
+ }
39
+ };
40
+ /**
41
+ * Error thrown when image format is not supported
42
+ */
43
+ var UnsupportedFormatError = class extends ImageError {
44
+ constructor(format, adapter) {
45
+ super(`Unsupported image format: ${format}`, "UNSUPPORTED_FORMAT", adapter);
46
+ this.name = "UnsupportedFormatError";
47
+ }
48
+ };
49
+ /**
50
+ * Error thrown when an operation is not supported by the adapter
51
+ */
52
+ var OperationNotSupportedError = class extends ImageError {
53
+ constructor(operation, adapter) {
54
+ super(`Operation not supported: ${operation}`, "OPERATION_NOT_SUPPORTED", adapter);
55
+ this.name = "OperationNotSupportedError";
56
+ }
57
+ };
58
+ /**
59
+ * Error thrown when image processing fails
60
+ */
61
+ var ProcessingError = class extends ImageError {
62
+ cause;
63
+ constructor(message, adapter, cause) {
64
+ super(message, "PROCESSING_ERROR", adapter);
65
+ this.cause = cause;
66
+ this.name = "ProcessingError";
67
+ }
68
+ };
69
+ /**
70
+ * Error thrown when adapter type is invalid
71
+ */
72
+ var InvalidAdapterError = class extends ImageError {
73
+ constructor(type) {
74
+ super(`Invalid adapter type: ${type}`, "INVALID_ADAPTER");
75
+ this.name = "InvalidAdapterError";
76
+ }
77
+ };
78
+ /**
79
+ * Error thrown when remote service (e.g., imgproxy) fails
80
+ */
81
+ var RemoteServiceError = class extends ImageError {
82
+ statusCode;
83
+ constructor(message, adapter, statusCode) {
84
+ super(message, "REMOTE_SERVICE_ERROR", adapter);
85
+ this.statusCode = statusCode;
86
+ this.name = "RemoteServiceError";
87
+ }
88
+ };
89
+ //#endregion
90
+ //#region src/adapters/imgproxy.ts
91
+ /**
92
+ * imgproxy adapter for remote image processing
93
+ *
94
+ * Uses a self-hosted imgproxy server for scalable image processing.
95
+ * Ideal for enterprise deployments where processing is offloaded to dedicated servers.
96
+ *
97
+ * @see https://imgproxy.net/
98
+ */
99
+ var imgproxy_exports = /* @__PURE__ */ __exportAll({
100
+ ImgproxyAdapter: () => ImgproxyAdapter,
101
+ signImgproxyUrl: () => signImgproxyUrl
102
+ });
103
+ /**
104
+ * Generate a signed imgproxy URL
105
+ *
106
+ * Standalone function for generating signed URLs without creating an adapter instance.
107
+ * Useful for client-side usage or when you only need URL generation.
108
+ *
109
+ * @param source - Source image URL (must be http/https)
110
+ * @param config - imgproxy server configuration
111
+ * @param options - Processing options
112
+ * @returns Signed imgproxy URL
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * import { signImgproxyUrl } from '@happyvertical/images';
117
+ *
118
+ * const url = signImgproxyUrl(
119
+ * 'https://example.com/image.jpg',
120
+ * {
121
+ * baseUrl: 'https://imgproxy.example.com',
122
+ * key: 'hex-encoded-key',
123
+ * salt: 'hex-encoded-salt'
124
+ * },
125
+ * { width: 300, height: 200, format: 'webp' }
126
+ * );
127
+ * ```
128
+ */
52
129
  function signImgproxyUrl(source, config, options = {}) {
53
- const adapter = new ImgproxyAdapter({
54
- type: "imgproxy",
55
- baseUrl: config.baseUrl,
56
- key: config.key,
57
- salt: config.salt
58
- });
59
- return adapter.getSignedUrl(source, options);
60
- }
61
- class ImgproxyAdapter {
62
- baseUrl;
63
- key;
64
- salt;
65
- constructor(options) {
66
- this.baseUrl = options.baseUrl.replace(/\/$/, "");
67
- if (options.key && options.salt) {
68
- this.key = Buffer.from(options.key, "hex");
69
- this.salt = Buffer.from(options.salt, "hex");
70
- }
71
- }
72
- /**
73
- * Generate a signed imgproxy URL without fetching the image
74
- *
75
- * This is useful for:
76
- * - Client-side usage where you want to display images directly
77
- * - Debugging URL signing issues
78
- * - Pre-generating URLs for batch processing
79
- *
80
- * @param source - Source image URL (must be http/https)
81
- * @param options - Processing options
82
- * @returns Signed imgproxy URL
83
- *
84
- * @example
85
- * ```typescript
86
- * const adapter = new ImgproxyAdapter({
87
- * type: 'imgproxy',
88
- * baseUrl: 'https://imgproxy.example.com',
89
- * key: 'hex-key',
90
- * salt: 'hex-salt'
91
- * });
92
- *
93
- * // Generate thumbnail URL
94
- * const url = adapter.getSignedUrl('https://example.com/image.jpg', {
95
- * width: 300,
96
- * height: 200,
97
- * fit: 'cover',
98
- * format: 'webp'
99
- * });
100
- * // Returns: https://imgproxy.example.com/SIGNATURE/rs:fill:300:200/aHR0cHM6Ly9.../image.webp
101
- * ```
102
- */
103
- getSignedUrl(source, options = {}) {
104
- if (!source.startsWith("http://") && !source.startsWith("https://")) {
105
- throw new ProcessingError(
106
- `imgproxy requires HTTP/HTTPS URLs as source. Got: ${source.substring(0, 50)}...`,
107
- "imgproxy"
108
- );
109
- }
110
- const resizeType = this.mapFitToResizeType(options.fit || "cover");
111
- const processing = this.buildProcessingString({
112
- resize: resizeType,
113
- width: options.width || 0,
114
- height: options.height || 0,
115
- quality: options.quality,
116
- format: options.format
117
- });
118
- return this.buildUrl(source, processing, options.format);
119
- }
120
- /**
121
- * Sign a path using HMAC-SHA256
122
- */
123
- sign(path) {
124
- if (!this.key || !this.salt) {
125
- return "unsafe";
126
- }
127
- const hmac = createHmac("sha256", this.key);
128
- hmac.update(this.salt);
129
- hmac.update(path);
130
- return hmac.digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").substring(0, 32);
131
- }
132
- /**
133
- * Encode source URL for imgproxy
134
- */
135
- encodeSource(source) {
136
- return Buffer.from(source).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
137
- }
138
- /**
139
- * Build imgproxy URL
140
- */
141
- buildUrl(source, processing, extension) {
142
- const encodedSource = this.encodeSource(source);
143
- const ext = extension ? `.${extension}` : "";
144
- const path = `/${processing}/${encodedSource}${ext}`;
145
- const signature = this.sign(path);
146
- return `${this.baseUrl}/${signature}${path}`;
147
- }
148
- /**
149
- * Convert ImageInput to URL string
150
- */
151
- toUrl(input) {
152
- if (Buffer.isBuffer(input)) {
153
- throw new ProcessingError(
154
- "imgproxy adapter requires URL input, not Buffer. Upload the buffer to a storage service first.",
155
- "imgproxy"
156
- );
157
- }
158
- if (!input.startsWith("http://") && !input.startsWith("https://")) {
159
- throw new ProcessingError(
160
- `imgproxy adapter requires HTTP/HTTPS URLs as input. Got: ${input.substring(0, 50)}...`,
161
- "imgproxy"
162
- );
163
- }
164
- return input;
165
- }
166
- async getDimensions(input) {
167
- const url = this.toUrl(input);
168
- try {
169
- const infoUrl = this.buildUrl(url, "info:1");
170
- const response = await fetch(infoUrl);
171
- if (!response.ok) {
172
- throw new RemoteServiceError(
173
- `imgproxy info request failed: ${response.statusText}`,
174
- "imgproxy",
175
- response.status
176
- );
177
- }
178
- const info = await response.json();
179
- return {
180
- width: info.width,
181
- height: info.height
182
- };
183
- } catch (error) {
184
- if (error instanceof ProcessingError || error instanceof RemoteServiceError) {
185
- throw error;
186
- }
187
- throw new ProcessingError(
188
- `Failed to get dimensions: ${error instanceof Error ? error.message : String(error)}`,
189
- "imgproxy",
190
- error instanceof Error ? error : void 0
191
- );
192
- }
193
- }
194
- async thumbnail(input, output, options = {}) {
195
- const url = this.toUrl(input);
196
- try {
197
- const processing = this.buildProcessingString({
198
- resize: "fit",
199
- width: options.maxWidth || 0,
200
- height: options.maxHeight || 0,
201
- quality: options.quality,
202
- format: options.format
203
- });
204
- const ext = options.format || this.inferFormat(output);
205
- const imgUrl = this.buildUrl(url, processing, ext);
206
- await this.fetchAndSave(imgUrl, output);
207
- } catch (error) {
208
- if (error instanceof ProcessingError || error instanceof RemoteServiceError) {
209
- throw error;
210
- }
211
- throw new ProcessingError(
212
- `Thumbnail generation failed: ${error instanceof Error ? error.message : String(error)}`,
213
- "imgproxy",
214
- error instanceof Error ? error : void 0
215
- );
216
- }
217
- }
218
- async convert(input, output, options = {}) {
219
- const url = this.toUrl(input);
220
- try {
221
- const format = options.format || this.inferFormat(output);
222
- const processing = this.buildProcessingString({
223
- quality: options.quality,
224
- format
225
- });
226
- const imgUrl = this.buildUrl(url, processing, format);
227
- await this.fetchAndSave(imgUrl, output);
228
- } catch (error) {
229
- if (error instanceof ProcessingError || error instanceof RemoteServiceError) {
230
- throw error;
231
- }
232
- throw new ProcessingError(
233
- `Format conversion failed: ${error instanceof Error ? error.message : String(error)}`,
234
- "imgproxy",
235
- error instanceof Error ? error : void 0
236
- );
237
- }
238
- }
239
- async getMetadata(input) {
240
- const url = this.toUrl(input);
241
- try {
242
- const infoUrl = this.buildUrl(url, "info:1");
243
- const response = await fetch(infoUrl);
244
- if (!response.ok) {
245
- throw new RemoteServiceError(
246
- `imgproxy info request failed: ${response.statusText}`,
247
- "imgproxy",
248
- response.status
249
- );
250
- }
251
- const info = await response.json();
252
- return {
253
- width: info.width,
254
- height: info.height,
255
- format: info.type || "unknown"
256
- // imgproxy returns limited metadata
257
- // Additional EXIF/IPTC/XMP not typically available
258
- };
259
- } catch (error) {
260
- if (error instanceof ProcessingError || error instanceof RemoteServiceError) {
261
- throw error;
262
- }
263
- throw new ProcessingError(
264
- `Failed to get metadata: ${error instanceof Error ? error.message : String(error)}`,
265
- "imgproxy",
266
- error instanceof Error ? error : void 0
267
- );
268
- }
269
- }
270
- async hash(_input, _algorithm = "perceptual") {
271
- throw new OperationNotSupportedError(
272
- "hash (imgproxy does not support direct hashing - fetch image first)",
273
- "imgproxy"
274
- );
275
- }
276
- async resize(input, output, options) {
277
- const url = this.toUrl(input);
278
- try {
279
- const resizeType = this.mapFitToResizeType(options.fit || "cover");
280
- const processing = this.buildProcessingString({
281
- resize: resizeType,
282
- width: options.width || 0,
283
- height: options.height || 0,
284
- quality: options.quality,
285
- format: options.format
286
- });
287
- const ext = options.format || this.inferFormat(output);
288
- const imgUrl = this.buildUrl(url, processing, ext);
289
- await this.fetchAndSave(imgUrl, output);
290
- } catch (error) {
291
- if (error instanceof ProcessingError || error instanceof RemoteServiceError) {
292
- throw error;
293
- }
294
- throw new ProcessingError(
295
- `Resize failed: ${error instanceof Error ? error.message : String(error)}`,
296
- "imgproxy",
297
- error instanceof Error ? error : void 0
298
- );
299
- }
300
- }
301
- /**
302
- * Build imgproxy processing string
303
- */
304
- buildProcessingString(opts) {
305
- const parts = [];
306
- if (opts.resize) {
307
- const w = opts.width || 0;
308
- const h = opts.height || 0;
309
- parts.push(`rs:${opts.resize}:${w}:${h}`);
310
- }
311
- if (opts.quality) {
312
- parts.push(`q:${opts.quality}`);
313
- }
314
- return parts.length > 0 ? parts.join("/") : "raw:1";
315
- }
316
- /**
317
- * Map fit mode to imgproxy resize type
318
- */
319
- mapFitToResizeType(fit) {
320
- const map = {
321
- cover: "fill",
322
- // Fill and crop
323
- contain: "fit",
324
- // Fit within
325
- fill: "force",
326
- // Force exact size
327
- inside: "fit",
328
- // Same as contain
329
- outside: "fill"
330
- // Same as cover
331
- };
332
- return map[fit] || "fit";
333
- }
334
- /**
335
- * Fetch image from imgproxy and save to file
336
- */
337
- async fetchAndSave(url, output) {
338
- const response = await fetch(url);
339
- if (!response.ok) {
340
- throw new RemoteServiceError(
341
- `imgproxy request failed: ${response.statusText}`,
342
- "imgproxy",
343
- response.status
344
- );
345
- }
346
- const buffer = Buffer.from(await response.arrayBuffer());
347
- const { writeFile } = await import("node:fs/promises");
348
- await writeFile(output, buffer);
349
- }
350
- /**
351
- * Infer image format from file extension
352
- */
353
- inferFormat(path) {
354
- const ext = path.split(".").pop()?.toLowerCase();
355
- const map = {
356
- jpg: "jpeg",
357
- jpeg: "jpeg",
358
- png: "png",
359
- webp: "webp",
360
- avif: "avif",
361
- gif: "gif",
362
- tiff: "tiff",
363
- tif: "tiff"
364
- };
365
- return map[ext || ""] || "jpeg";
366
- }
367
- }
368
- const imgproxy = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
369
- __proto__: null,
370
- ImgproxyAdapter,
371
- signImgproxyUrl
372
- }, Symbol.toStringTag, { value: "Module" }));
373
- class JimpAdapter {
374
- jimpStatic = null;
375
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Stored for API compatibility and future extensibility
376
- options;
377
- /**
378
- * Create a new Jimp adapter
379
- * @param options - Jimp adapter options
380
- */
381
- constructor(options = { type: "jimp" }) {
382
- this.options = options;
383
- }
384
- /**
385
- * Lazily load Jimp module
386
- */
387
- async getJimp() {
388
- if (!this.jimpStatic) {
389
- try {
390
- const jimpModule = await import("jimp");
391
- this.jimpStatic = jimpModule.Jimp;
392
- } catch (error) {
393
- throw new ProcessingError(
394
- "Jimp library is not installed. Install it with: npm install jimp",
395
- "jimp",
396
- error instanceof Error ? error : void 0
397
- );
398
- }
399
- }
400
- return this.jimpStatic;
401
- }
402
- async getDimensions(input) {
403
- const Jimp = await this.getJimp();
404
- try {
405
- const image = await Jimp.read(input);
406
- return { width: image.width, height: image.height };
407
- } catch (error) {
408
- throw this.mapError(error, input);
409
- }
410
- }
411
- async thumbnail(input, output, options = {}) {
412
- const Jimp = await this.getJimp();
413
- try {
414
- const image = await Jimp.read(input);
415
- if (options.maxWidth && options.maxHeight) {
416
- image.scaleToFit({ w: options.maxWidth, h: options.maxHeight });
417
- } else if (options.maxWidth) {
418
- const scale = options.maxWidth / image.width;
419
- if (scale < 1) {
420
- image.scale(scale);
421
- }
422
- } else if (options.maxHeight) {
423
- const scale = options.maxHeight / image.height;
424
- if (scale < 1) {
425
- image.scale(scale);
426
- }
427
- }
428
- await this.writeWithOptions(image, output, options.quality);
429
- } catch (error) {
430
- if (error instanceof ProcessingError || error instanceof ImageNotFoundError) {
431
- throw error;
432
- }
433
- throw this.mapError(error, input);
434
- }
435
- }
436
- async convert(input, output, options = {}) {
437
- const Jimp = await this.getJimp();
438
- try {
439
- const image = await Jimp.read(input);
440
- await this.writeWithOptions(image, output, options.quality);
441
- } catch (error) {
442
- throw this.mapError(error, input);
443
- }
444
- }
445
- async getMetadata(input) {
446
- const Jimp = await this.getJimp();
447
- try {
448
- const image = await Jimp.read(input);
449
- const mime = this.getMimeFromInput(input);
450
- const format = this.formatFromMime(mime);
451
- return {
452
- width: image.width,
453
- height: image.height,
454
- format,
455
- channels: 4,
456
- // Jimp uses RGBA internally
457
- hasAlpha: true
458
- // Jimp always works with alpha channel
459
- // Jimp has limited metadata extraction
460
- // EXIF/IPTC/XMP not directly supported
461
- };
462
- } catch (error) {
463
- throw this.mapError(error, input);
464
- }
465
- }
466
- async hash(input, algorithm = "perceptual") {
467
- const Jimp = await this.getJimp();
468
- try {
469
- if (algorithm === "md5" || algorithm === "sha256") {
470
- const { createHash } = await import("node:crypto");
471
- const image2 = await Jimp.read(input);
472
- const buffer = await image2.getBuffer("image/png");
473
- return createHash(algorithm).update(buffer).digest("hex");
474
- }
475
- const image = await Jimp.read(input);
476
- const resized = image.clone().resize({ w: 8, h: 8 }).greyscale();
477
- const data = resized.bitmap.data;
478
- let sum = 0;
479
- const pixels = [];
480
- for (let i = 0; i < data.length; i += 4) {
481
- const gray = data[i];
482
- pixels.push(gray);
483
- sum += gray;
484
- }
485
- const avg = sum / pixels.length;
486
- let hash = "";
487
- for (const val of pixels) {
488
- hash += val >= avg ? "1" : "0";
489
- }
490
- return BigInt(`0b${hash}`).toString(16).padStart(16, "0");
491
- } catch (error) {
492
- throw this.mapError(error, input);
493
- }
494
- }
495
- async resize(input, output, options) {
496
- const Jimp = await this.getJimp();
497
- try {
498
- const image = await Jimp.read(input);
499
- const fit = options.fit || "cover";
500
- if (options.width && options.height) {
501
- if (fit === "cover") {
502
- image.cover({ w: options.width, h: options.height });
503
- } else if (fit === "contain" || fit === "inside") {
504
- image.contain({ w: options.width, h: options.height });
505
- } else {
506
- image.resize({ w: options.width, h: options.height });
507
- }
508
- } else if (options.width) {
509
- const scale = options.width / image.width;
510
- image.scale(scale);
511
- } else if (options.height) {
512
- const scale = options.height / image.height;
513
- image.scale(scale);
514
- }
515
- await this.writeWithOptions(image, output, options.quality);
516
- } catch (error) {
517
- throw this.mapError(error, input);
518
- }
519
- }
520
- /**
521
- * Write image with quality settings based on output format
522
- */
523
- async writeWithOptions(image, output, quality) {
524
- const format = this.inferFormat(output);
525
- const mime = this.mimeFromFormat(format);
526
- if (format === "avif") {
527
- throw new UnsupportedFormatError("avif", "jimp");
528
- }
529
- const buffer = await image.getBuffer(mime, {
530
- quality: quality ?? 80
531
- });
532
- const { writeFile } = await import("node:fs/promises");
533
- await writeFile(output, buffer);
534
- }
535
- /**
536
- * Infer image format from file extension
537
- */
538
- inferFormat(path) {
539
- const ext = path.split(".").pop()?.toLowerCase();
540
- const map = {
541
- jpg: "jpeg",
542
- jpeg: "jpeg",
543
- png: "png",
544
- webp: "webp",
545
- avif: "avif",
546
- gif: "gif",
547
- tiff: "tiff",
548
- tif: "tiff",
549
- bmp: "png"
550
- // Convert BMP to PNG
551
- };
552
- return map[ext || ""] || "jpeg";
553
- }
554
- /**
555
- * Get MIME type from format
556
- */
557
- mimeFromFormat(format) {
558
- const map = {
559
- jpeg: "image/jpeg",
560
- png: "image/png",
561
- webp: "image/webp",
562
- avif: "image/avif",
563
- gif: "image/gif",
564
- tiff: "image/tiff"
565
- };
566
- return map[format] || "image/jpeg";
567
- }
568
- /**
569
- * Get format from MIME type
570
- */
571
- formatFromMime(mime) {
572
- const map = {
573
- "image/jpeg": "jpeg",
574
- "image/png": "png",
575
- "image/webp": "webp",
576
- "image/gif": "gif",
577
- "image/tiff": "tiff",
578
- "image/bmp": "bmp"
579
- };
580
- return map[mime] || "unknown";
581
- }
582
- /**
583
- * Try to determine MIME type from input
584
- */
585
- getMimeFromInput(input) {
586
- if (typeof input === "string") {
587
- const format = this.inferFormat(input);
588
- return this.mimeFromFormat(format);
589
- }
590
- return "image/png";
591
- }
592
- /**
593
- * Map Jimp errors to our error types
594
- */
595
- mapError(error, input) {
596
- const message = error instanceof Error ? error.message : String(error);
597
- if (message.includes("no such file") || message.includes("ENOENT") || message.includes("Could not find")) {
598
- const path = typeof input === "string" ? input : "<buffer>";
599
- throw new ImageNotFoundError(path, "jimp");
600
- }
601
- if (message.includes("not supported") || message.includes("Could not find MIME")) {
602
- throw new UnsupportedFormatError("unknown", "jimp");
603
- }
604
- return new ProcessingError(
605
- `Image processing failed: ${message}`,
606
- "jimp",
607
- error instanceof Error ? error : void 0
608
- );
609
- }
130
+ return new ImgproxyAdapter({
131
+ type: "imgproxy",
132
+ baseUrl: config.baseUrl,
133
+ key: config.key,
134
+ salt: config.salt
135
+ }).getSignedUrl(source, options);
610
136
  }
611
- const jimp = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
612
- __proto__: null,
613
- JimpAdapter
614
- }, Symbol.toStringTag, { value: "Module" }));
615
- class SharpAdapter {
616
- sharp = null;
617
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Stored for API compatibility and future extensibility
618
- options;
619
- /**
620
- * Create a new Sharp adapter
621
- * @param options - Sharp adapter options
622
- */
623
- constructor(options = {}) {
624
- this.options = options;
625
- }
626
- /**
627
- * Lazily load sharp module
628
- */
629
- async getSharp() {
630
- if (!this.sharp) {
631
- try {
632
- this.sharp = (await import("sharp")).default;
633
- } catch (error) {
634
- throw new ProcessingError(
635
- "Sharp library is not installed. Install it with: npm install sharp",
636
- "sharp",
637
- error instanceof Error ? error : void 0
638
- );
639
- }
640
- }
641
- return this.sharp;
642
- }
643
- async getDimensions(input) {
644
- const sharp2 = await this.getSharp();
645
- try {
646
- const metadata = await sharp2(input).metadata();
647
- if (!metadata.width || !metadata.height) {
648
- throw new ProcessingError(
649
- "Could not determine image dimensions",
650
- "sharp"
651
- );
652
- }
653
- return { width: metadata.width, height: metadata.height };
654
- } catch (error) {
655
- if (error instanceof ProcessingError) throw error;
656
- throw this.mapError(error, input);
657
- }
658
- }
659
- async thumbnail(input, output, options = {}) {
660
- const sharp2 = await this.getSharp();
661
- try {
662
- let pipeline = sharp2(input).resize({
663
- width: options.maxWidth,
664
- height: options.maxHeight,
665
- fit: options.fit || "inside",
666
- withoutEnlargement: true
667
- });
668
- if (options.format) {
669
- pipeline = pipeline.toFormat(options.format, {
670
- quality: options.quality
671
- });
672
- } else if (options.quality) {
673
- const format = this.inferFormat(output);
674
- pipeline = pipeline.toFormat(format, { quality: options.quality });
675
- }
676
- await pipeline.toFile(output);
677
- } catch (error) {
678
- throw this.mapError(error, input);
679
- }
680
- }
681
- async convert(input, output, options = {}) {
682
- const sharp2 = await this.getSharp();
683
- try {
684
- const format = options.format || this.inferFormat(output);
685
- await sharp2(input).toFormat(format, { quality: options.quality }).toFile(output);
686
- } catch (error) {
687
- throw this.mapError(error, input);
688
- }
689
- }
690
- async getMetadata(input) {
691
- const sharp2 = await this.getSharp();
692
- try {
693
- const meta = await sharp2(input).metadata();
694
- if (!meta.width || !meta.height || !meta.format) {
695
- throw new ProcessingError("Could not extract image metadata", "sharp");
696
- }
697
- return {
698
- width: meta.width,
699
- height: meta.height,
700
- format: meta.format,
701
- space: meta.space,
702
- channels: meta.channels,
703
- depth: meta.depth,
704
- density: meta.density,
705
- hasAlpha: meta.hasAlpha,
706
- orientation: meta.orientation,
707
- exif: meta.exif ? this.parseExifBuffer(meta.exif) : void 0,
708
- iptc: meta.iptc ? this.parseIptcBuffer(meta.iptc) : void 0,
709
- xmp: meta.xmp ? this.parseXmpBuffer(meta.xmp) : void 0
710
- };
711
- } catch (error) {
712
- if (error instanceof ProcessingError) throw error;
713
- throw this.mapError(error, input);
714
- }
715
- }
716
- async hash(input, algorithm = "perceptual") {
717
- const sharp2 = await this.getSharp();
718
- try {
719
- if (algorithm === "md5" || algorithm === "sha256") {
720
- const { createHash } = await import("node:crypto");
721
- const buffer = await sharp2(input).toBuffer();
722
- return createHash(algorithm).update(buffer).digest("hex");
723
- }
724
- const { data } = await sharp2(input).resize(8, 8, { fit: "fill" }).grayscale().raw().toBuffer({ resolveWithObject: true });
725
- const avg = data.reduce((sum, val) => sum + val, 0) / data.length;
726
- let hash = "";
727
- for (const val of data) {
728
- hash += val >= avg ? "1" : "0";
729
- }
730
- return BigInt(`0b${hash}`).toString(16).padStart(16, "0");
731
- } catch (error) {
732
- throw this.mapError(error, input);
733
- }
734
- }
735
- async resize(input, output, options) {
736
- const sharp2 = await this.getSharp();
737
- try {
738
- let pipeline = sharp2(input).resize({
739
- width: options.width,
740
- height: options.height,
741
- fit: options.fit || "cover"
742
- });
743
- if (options.format) {
744
- pipeline = pipeline.toFormat(options.format, {
745
- quality: options.quality
746
- });
747
- } else if (options.quality) {
748
- const format = this.inferFormat(output);
749
- pipeline = pipeline.toFormat(format, { quality: options.quality });
750
- }
751
- await pipeline.toFile(output);
752
- } catch (error) {
753
- throw this.mapError(error, input);
754
- }
755
- }
756
- /**
757
- * Infer image format from file extension
758
- */
759
- inferFormat(path) {
760
- const ext = path.split(".").pop()?.toLowerCase();
761
- const map = {
762
- jpg: "jpeg",
763
- jpeg: "jpeg",
764
- png: "png",
765
- webp: "webp",
766
- avif: "avif",
767
- gif: "gif",
768
- tiff: "tiff",
769
- tif: "tiff"
770
- };
771
- return map[ext || ""] || "jpeg";
772
- }
773
- /**
774
- * Parse EXIF buffer to object (basic parsing)
775
- */
776
- parseExifBuffer(buffer) {
777
- return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
778
- }
779
- /**
780
- * Parse IPTC buffer to object (basic parsing)
781
- */
782
- parseIptcBuffer(buffer) {
783
- return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
784
- }
785
- /**
786
- * Parse XMP buffer to object (basic parsing)
787
- */
788
- parseXmpBuffer(buffer) {
789
- try {
790
- const xmpString = buffer.toString("utf8");
791
- return { raw: `${xmpString.slice(0, 500)}...` };
792
- } catch {
793
- return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
794
- }
795
- }
796
- /**
797
- * Map sharp errors to our error types
798
- */
799
- mapError(error, input) {
800
- const message = error instanceof Error ? error.message : String(error);
801
- if (message.includes("Input file is missing")) {
802
- const path = typeof input === "string" ? input : "<buffer>";
803
- throw new ImageNotFoundError(path, "sharp");
804
- }
805
- if (message.includes("Input buffer contains unsupported image format")) {
806
- throw new ProcessingError("Unsupported image format", "sharp");
807
- }
808
- return new ProcessingError(
809
- `Image processing failed: ${message}`,
810
- "sharp",
811
- error instanceof Error ? error : void 0
812
- );
813
- }
814
- }
815
- const sharp = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
816
- __proto__: null,
817
- SharpAdapter
818
- }, Symbol.toStringTag, { value: "Module" }));
819
- let cachedFontData = null;
820
- let cachedFontName = null;
137
+ /**
138
+ * imgproxy adapter for remote image processing
139
+ *
140
+ * Offloads image processing to a self-hosted imgproxy server.
141
+ * Supports URL signing for secure access.
142
+ *
143
+ * Features:
144
+ * - Scalable remote processing
145
+ * - URL-based API
146
+ * - Signed URLs for security
147
+ * - CDN-friendly outputs
148
+ *
149
+ * Limitations:
150
+ * - Requires network access to imgproxy server
151
+ * - Input must be accessible via URL (not local paths)
152
+ * - Hash computation not directly supported (requires fetching image)
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const processor = new ImgproxyAdapter({
157
+ * type: 'imgproxy',
158
+ * baseUrl: 'https://imgproxy.example.com',
159
+ * key: 'hex-encoded-key',
160
+ * salt: 'hex-encoded-salt'
161
+ * });
162
+ *
163
+ * await processor.thumbnail(
164
+ * 'https://example.com/image.jpg',
165
+ * '/tmp/thumb.jpg',
166
+ * { maxWidth: 300 }
167
+ * );
168
+ * ```
169
+ */
170
+ var ImgproxyAdapter = class {
171
+ baseUrl;
172
+ key;
173
+ salt;
174
+ constructor(options) {
175
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
176
+ if (options.key && options.salt) {
177
+ this.key = Buffer.from(options.key, "hex");
178
+ this.salt = Buffer.from(options.salt, "hex");
179
+ }
180
+ }
181
+ /**
182
+ * Generate a signed imgproxy URL without fetching the image
183
+ *
184
+ * This is useful for:
185
+ * - Client-side usage where you want to display images directly
186
+ * - Debugging URL signing issues
187
+ * - Pre-generating URLs for batch processing
188
+ *
189
+ * @param source - Source image URL (must be http/https)
190
+ * @param options - Processing options
191
+ * @returns Signed imgproxy URL
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const adapter = new ImgproxyAdapter({
196
+ * type: 'imgproxy',
197
+ * baseUrl: 'https://imgproxy.example.com',
198
+ * key: 'hex-key',
199
+ * salt: 'hex-salt'
200
+ * });
201
+ *
202
+ * // Generate thumbnail URL
203
+ * const url = adapter.getSignedUrl('https://example.com/image.jpg', {
204
+ * width: 300,
205
+ * height: 200,
206
+ * fit: 'cover',
207
+ * format: 'webp'
208
+ * });
209
+ * // Returns: https://imgproxy.example.com/SIGNATURE/rs:fill:300:200/aHR0cHM6Ly9.../image.webp
210
+ * ```
211
+ */
212
+ getSignedUrl(source, options = {}) {
213
+ if (!source.startsWith("http://") && !source.startsWith("https://")) throw new ProcessingError(`imgproxy requires HTTP/HTTPS URLs as source. Got: ${source.substring(0, 50)}...`, "imgproxy");
214
+ const resizeType = this.mapFitToResizeType(options.fit || "cover");
215
+ const processing = this.buildProcessingString({
216
+ resize: resizeType,
217
+ width: options.width || 0,
218
+ height: options.height || 0,
219
+ quality: options.quality,
220
+ format: options.format
221
+ });
222
+ return this.buildUrl(source, processing, options.format);
223
+ }
224
+ /**
225
+ * Sign a path using HMAC-SHA256
226
+ */
227
+ sign(path) {
228
+ if (!this.key || !this.salt) return "unsafe";
229
+ const hmac = createHmac("sha256", this.key);
230
+ hmac.update(this.salt);
231
+ hmac.update(path);
232
+ return hmac.digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").substring(0, 32);
233
+ }
234
+ /**
235
+ * Encode source URL for imgproxy
236
+ */
237
+ encodeSource(source) {
238
+ return Buffer.from(source).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
239
+ }
240
+ /**
241
+ * Build imgproxy URL
242
+ */
243
+ buildUrl(source, processing, extension) {
244
+ const path = `/${processing}/${this.encodeSource(source)}${extension ? `.${extension}` : ""}`;
245
+ const signature = this.sign(path);
246
+ return `${this.baseUrl}/${signature}${path}`;
247
+ }
248
+ /**
249
+ * Convert ImageInput to URL string
250
+ */
251
+ toUrl(input) {
252
+ if (Buffer.isBuffer(input)) throw new ProcessingError("imgproxy adapter requires URL input, not Buffer. Upload the buffer to a storage service first.", "imgproxy");
253
+ if (!input.startsWith("http://") && !input.startsWith("https://")) throw new ProcessingError(`imgproxy adapter requires HTTP/HTTPS URLs as input. Got: ${input.substring(0, 50)}...`, "imgproxy");
254
+ return input;
255
+ }
256
+ async getDimensions(input) {
257
+ const url = this.toUrl(input);
258
+ try {
259
+ const infoUrl = this.buildUrl(url, "info:1");
260
+ const response = await fetch(infoUrl);
261
+ if (!response.ok) throw new RemoteServiceError(`imgproxy info request failed: ${response.statusText}`, "imgproxy", response.status);
262
+ const info = await response.json();
263
+ return {
264
+ width: info.width,
265
+ height: info.height
266
+ };
267
+ } catch (error) {
268
+ if (error instanceof ProcessingError || error instanceof RemoteServiceError) throw error;
269
+ throw new ProcessingError(`Failed to get dimensions: ${error instanceof Error ? error.message : String(error)}`, "imgproxy", error instanceof Error ? error : void 0);
270
+ }
271
+ }
272
+ async thumbnail(input, output, options = {}) {
273
+ const url = this.toUrl(input);
274
+ try {
275
+ const processing = this.buildProcessingString({
276
+ resize: "fit",
277
+ width: options.maxWidth || 0,
278
+ height: options.maxHeight || 0,
279
+ quality: options.quality,
280
+ format: options.format
281
+ });
282
+ const ext = options.format || this.inferFormat(output);
283
+ const imgUrl = this.buildUrl(url, processing, ext);
284
+ await this.fetchAndSave(imgUrl, output);
285
+ } catch (error) {
286
+ if (error instanceof ProcessingError || error instanceof RemoteServiceError) throw error;
287
+ throw new ProcessingError(`Thumbnail generation failed: ${error instanceof Error ? error.message : String(error)}`, "imgproxy", error instanceof Error ? error : void 0);
288
+ }
289
+ }
290
+ async convert(input, output, options = {}) {
291
+ const url = this.toUrl(input);
292
+ try {
293
+ const format = options.format || this.inferFormat(output);
294
+ const processing = this.buildProcessingString({
295
+ quality: options.quality,
296
+ format
297
+ });
298
+ const imgUrl = this.buildUrl(url, processing, format);
299
+ await this.fetchAndSave(imgUrl, output);
300
+ } catch (error) {
301
+ if (error instanceof ProcessingError || error instanceof RemoteServiceError) throw error;
302
+ throw new ProcessingError(`Format conversion failed: ${error instanceof Error ? error.message : String(error)}`, "imgproxy", error instanceof Error ? error : void 0);
303
+ }
304
+ }
305
+ async getMetadata(input) {
306
+ const url = this.toUrl(input);
307
+ try {
308
+ const infoUrl = this.buildUrl(url, "info:1");
309
+ const response = await fetch(infoUrl);
310
+ if (!response.ok) throw new RemoteServiceError(`imgproxy info request failed: ${response.statusText}`, "imgproxy", response.status);
311
+ const info = await response.json();
312
+ return {
313
+ width: info.width,
314
+ height: info.height,
315
+ format: info.type || "unknown"
316
+ };
317
+ } catch (error) {
318
+ if (error instanceof ProcessingError || error instanceof RemoteServiceError) throw error;
319
+ throw new ProcessingError(`Failed to get metadata: ${error instanceof Error ? error.message : String(error)}`, "imgproxy", error instanceof Error ? error : void 0);
320
+ }
321
+ }
322
+ async hash(_input, _algorithm = "perceptual") {
323
+ throw new OperationNotSupportedError("hash (imgproxy does not support direct hashing - fetch image first)", "imgproxy");
324
+ }
325
+ async resize(input, output, options) {
326
+ const url = this.toUrl(input);
327
+ try {
328
+ const resizeType = this.mapFitToResizeType(options.fit || "cover");
329
+ const processing = this.buildProcessingString({
330
+ resize: resizeType,
331
+ width: options.width || 0,
332
+ height: options.height || 0,
333
+ quality: options.quality,
334
+ format: options.format
335
+ });
336
+ const ext = options.format || this.inferFormat(output);
337
+ const imgUrl = this.buildUrl(url, processing, ext);
338
+ await this.fetchAndSave(imgUrl, output);
339
+ } catch (error) {
340
+ if (error instanceof ProcessingError || error instanceof RemoteServiceError) throw error;
341
+ throw new ProcessingError(`Resize failed: ${error instanceof Error ? error.message : String(error)}`, "imgproxy", error instanceof Error ? error : void 0);
342
+ }
343
+ }
344
+ /**
345
+ * Build imgproxy processing string
346
+ */
347
+ buildProcessingString(opts) {
348
+ const parts = [];
349
+ if (opts.resize) {
350
+ const w = opts.width || 0;
351
+ const h = opts.height || 0;
352
+ parts.push(`rs:${opts.resize}:${w}:${h}`);
353
+ }
354
+ if (opts.quality) parts.push(`q:${opts.quality}`);
355
+ return parts.length > 0 ? parts.join("/") : "raw:1";
356
+ }
357
+ /**
358
+ * Map fit mode to imgproxy resize type
359
+ */
360
+ mapFitToResizeType(fit) {
361
+ return {
362
+ cover: "fill",
363
+ contain: "fit",
364
+ fill: "force",
365
+ inside: "fit",
366
+ outside: "fill"
367
+ }[fit] || "fit";
368
+ }
369
+ /**
370
+ * Fetch image from imgproxy and save to file
371
+ */
372
+ async fetchAndSave(url, output) {
373
+ const response = await fetch(url);
374
+ if (!response.ok) throw new RemoteServiceError(`imgproxy request failed: ${response.statusText}`, "imgproxy", response.status);
375
+ const buffer = Buffer.from(await response.arrayBuffer());
376
+ const { writeFile } = await import("node:fs/promises");
377
+ await writeFile(output, buffer);
378
+ }
379
+ /**
380
+ * Infer image format from file extension
381
+ */
382
+ inferFormat(path) {
383
+ return {
384
+ jpg: "jpeg",
385
+ jpeg: "jpeg",
386
+ png: "png",
387
+ webp: "webp",
388
+ avif: "avif",
389
+ gif: "gif",
390
+ tiff: "tiff",
391
+ tif: "tiff"
392
+ }[path.split(".").pop()?.toLowerCase() || ""] || "jpeg";
393
+ }
394
+ };
395
+ //#endregion
396
+ //#region src/adapters/jimp.ts
397
+ /**
398
+ * Jimp adapter for image processing
399
+ * Uses pure JavaScript for image processing (no native dependencies)
400
+ * Ideal for serverless/edge environments where Sharp cannot be used
401
+ */
402
+ var jimp_exports = /* @__PURE__ */ __exportAll({ JimpAdapter: () => JimpAdapter });
403
+ /**
404
+ * Jimp adapter for pure JavaScript image processing
405
+ *
406
+ * Provides a fallback option when Sharp (native library) is not available.
407
+ * Useful for:
408
+ * - Serverless environments (AWS Lambda, Vercel Edge)
409
+ * - Environments without native compilation support
410
+ * - Cross-platform compatibility
411
+ *
412
+ * Limitations compared to Sharp:
413
+ * - Slower processing speed
414
+ * - Limited EXIF/metadata extraction
415
+ * - No AVIF support
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * const processor = new JimpAdapter();
420
+ * const dims = await processor.getDimensions('/path/to/image.jpg');
421
+ * console.log(dims); // { width: 1920, height: 1080 }
422
+ * ```
423
+ */
424
+ var JimpAdapter = class {
425
+ jimpStatic = null;
426
+ options;
427
+ /**
428
+ * Create a new Jimp adapter
429
+ * @param options - Jimp adapter options
430
+ */
431
+ constructor(options = { type: "jimp" }) {
432
+ this.options = options;
433
+ }
434
+ /**
435
+ * Lazily load Jimp module
436
+ */
437
+ async getJimp() {
438
+ if (!this.jimpStatic) try {
439
+ const jimpModule = await import("jimp");
440
+ this.jimpStatic = jimpModule.Jimp;
441
+ } catch (error) {
442
+ throw new ProcessingError("Jimp library is not installed. Install it with: npm install jimp", "jimp", error instanceof Error ? error : void 0);
443
+ }
444
+ return this.jimpStatic;
445
+ }
446
+ async getDimensions(input) {
447
+ const Jimp = await this.getJimp();
448
+ try {
449
+ const image = await Jimp.read(input);
450
+ return {
451
+ width: image.width,
452
+ height: image.height
453
+ };
454
+ } catch (error) {
455
+ throw this.mapError(error, input);
456
+ }
457
+ }
458
+ async thumbnail(input, output, options = {}) {
459
+ const Jimp = await this.getJimp();
460
+ try {
461
+ const image = await Jimp.read(input);
462
+ if (options.maxWidth && options.maxHeight) image.scaleToFit({
463
+ w: options.maxWidth,
464
+ h: options.maxHeight
465
+ });
466
+ else if (options.maxWidth) {
467
+ const scale = options.maxWidth / image.width;
468
+ if (scale < 1) image.scale(scale);
469
+ } else if (options.maxHeight) {
470
+ const scale = options.maxHeight / image.height;
471
+ if (scale < 1) image.scale(scale);
472
+ }
473
+ await this.writeWithOptions(image, output, options.quality);
474
+ } catch (error) {
475
+ if (error instanceof ProcessingError || error instanceof ImageNotFoundError) throw error;
476
+ throw this.mapError(error, input);
477
+ }
478
+ }
479
+ async convert(input, output, options = {}) {
480
+ const Jimp = await this.getJimp();
481
+ try {
482
+ const image = await Jimp.read(input);
483
+ await this.writeWithOptions(image, output, options.quality);
484
+ } catch (error) {
485
+ throw this.mapError(error, input);
486
+ }
487
+ }
488
+ async getMetadata(input) {
489
+ const Jimp = await this.getJimp();
490
+ try {
491
+ const image = await Jimp.read(input);
492
+ const mime = this.getMimeFromInput(input);
493
+ const format = this.formatFromMime(mime);
494
+ return {
495
+ width: image.width,
496
+ height: image.height,
497
+ format,
498
+ channels: 4,
499
+ hasAlpha: true
500
+ };
501
+ } catch (error) {
502
+ throw this.mapError(error, input);
503
+ }
504
+ }
505
+ async hash(input, algorithm = "perceptual") {
506
+ const Jimp = await this.getJimp();
507
+ try {
508
+ if (algorithm === "md5" || algorithm === "sha256") {
509
+ const { createHash } = await import("node:crypto");
510
+ const buffer = await (await Jimp.read(input)).getBuffer("image/png");
511
+ return createHash(algorithm).update(buffer).digest("hex");
512
+ }
513
+ const data = (await Jimp.read(input)).clone().resize({
514
+ w: 8,
515
+ h: 8
516
+ }).greyscale().bitmap.data;
517
+ let sum = 0;
518
+ const pixels = [];
519
+ for (let i = 0; i < data.length; i += 4) {
520
+ const gray = data[i];
521
+ pixels.push(gray);
522
+ sum += gray;
523
+ }
524
+ const avg = sum / pixels.length;
525
+ let hash = "";
526
+ for (const val of pixels) hash += val >= avg ? "1" : "0";
527
+ return BigInt(`0b${hash}`).toString(16).padStart(16, "0");
528
+ } catch (error) {
529
+ throw this.mapError(error, input);
530
+ }
531
+ }
532
+ async resize(input, output, options) {
533
+ const Jimp = await this.getJimp();
534
+ try {
535
+ const image = await Jimp.read(input);
536
+ const fit = options.fit || "cover";
537
+ if (options.width && options.height) if (fit === "cover") image.cover({
538
+ w: options.width,
539
+ h: options.height
540
+ });
541
+ else if (fit === "contain" || fit === "inside") image.contain({
542
+ w: options.width,
543
+ h: options.height
544
+ });
545
+ else image.resize({
546
+ w: options.width,
547
+ h: options.height
548
+ });
549
+ else if (options.width) {
550
+ const scale = options.width / image.width;
551
+ image.scale(scale);
552
+ } else if (options.height) {
553
+ const scale = options.height / image.height;
554
+ image.scale(scale);
555
+ }
556
+ await this.writeWithOptions(image, output, options.quality);
557
+ } catch (error) {
558
+ throw this.mapError(error, input);
559
+ }
560
+ }
561
+ /**
562
+ * Write image with quality settings based on output format
563
+ */
564
+ async writeWithOptions(image, output, quality) {
565
+ const format = this.inferFormat(output);
566
+ const mime = this.mimeFromFormat(format);
567
+ if (format === "avif") throw new UnsupportedFormatError("avif", "jimp");
568
+ const buffer = await image.getBuffer(mime, { quality: quality ?? 80 });
569
+ const { writeFile } = await import("node:fs/promises");
570
+ await writeFile(output, buffer);
571
+ }
572
+ /**
573
+ * Infer image format from file extension
574
+ */
575
+ inferFormat(path) {
576
+ return {
577
+ jpg: "jpeg",
578
+ jpeg: "jpeg",
579
+ png: "png",
580
+ webp: "webp",
581
+ avif: "avif",
582
+ gif: "gif",
583
+ tiff: "tiff",
584
+ tif: "tiff",
585
+ bmp: "png"
586
+ }[path.split(".").pop()?.toLowerCase() || ""] || "jpeg";
587
+ }
588
+ /**
589
+ * Get MIME type from format
590
+ */
591
+ mimeFromFormat(format) {
592
+ return {
593
+ jpeg: "image/jpeg",
594
+ png: "image/png",
595
+ webp: "image/webp",
596
+ avif: "image/avif",
597
+ gif: "image/gif",
598
+ tiff: "image/tiff"
599
+ }[format] || "image/jpeg";
600
+ }
601
+ /**
602
+ * Get format from MIME type
603
+ */
604
+ formatFromMime(mime) {
605
+ return {
606
+ "image/jpeg": "jpeg",
607
+ "image/png": "png",
608
+ "image/webp": "webp",
609
+ "image/gif": "gif",
610
+ "image/tiff": "tiff",
611
+ "image/bmp": "bmp"
612
+ }[mime] || "unknown";
613
+ }
614
+ /**
615
+ * Try to determine MIME type from input
616
+ */
617
+ getMimeFromInput(input) {
618
+ if (typeof input === "string") {
619
+ const format = this.inferFormat(input);
620
+ return this.mimeFromFormat(format);
621
+ }
622
+ return "image/png";
623
+ }
624
+ /**
625
+ * Map Jimp errors to our error types
626
+ */
627
+ mapError(error, input) {
628
+ const message = error instanceof Error ? error.message : String(error);
629
+ if (message.includes("no such file") || message.includes("ENOENT") || message.includes("Could not find")) throw new ImageNotFoundError(typeof input === "string" ? input : "<buffer>", "jimp");
630
+ if (message.includes("not supported") || message.includes("Could not find MIME")) throw new UnsupportedFormatError("unknown", "jimp");
631
+ return new ProcessingError(`Image processing failed: ${message}`, "jimp", error instanceof Error ? error : void 0);
632
+ }
633
+ };
634
+ //#endregion
635
+ //#region src/adapters/sharp.ts
636
+ /**
637
+ * Sharp adapter for image processing
638
+ * Uses the native sharp library for fast image processing
639
+ */
640
+ var sharp_exports = /* @__PURE__ */ __exportAll({ SharpAdapter: () => SharpAdapter });
641
+ /**
642
+ * Sharp adapter for high-performance image processing
643
+ *
644
+ * Uses the native sharp library which provides:
645
+ * - Fast image resizing and format conversion
646
+ * - Rich metadata extraction including EXIF, IPTC, XMP
647
+ * - Perceptual hashing for image deduplication
648
+ *
649
+ * @example
650
+ * ```typescript
651
+ * const processor = new SharpAdapter();
652
+ * const dims = await processor.getDimensions('/path/to/image.jpg');
653
+ * console.log(dims); // { width: 1920, height: 1080 }
654
+ * ```
655
+ */
656
+ var SharpAdapter = class {
657
+ sharp = null;
658
+ options;
659
+ /**
660
+ * Create a new Sharp adapter
661
+ * @param options - Sharp adapter options
662
+ */
663
+ constructor(options = {}) {
664
+ this.options = options;
665
+ }
666
+ /**
667
+ * Lazily load sharp module
668
+ */
669
+ async getSharp() {
670
+ if (!this.sharp) try {
671
+ this.sharp = (await import("sharp")).default;
672
+ } catch (error) {
673
+ throw new ProcessingError("Sharp library is not installed. Install it with: npm install sharp", "sharp", error instanceof Error ? error : void 0);
674
+ }
675
+ return this.sharp;
676
+ }
677
+ async getDimensions(input) {
678
+ const sharp = await this.getSharp();
679
+ try {
680
+ const metadata = await sharp(input).metadata();
681
+ if (!metadata.width || !metadata.height) throw new ProcessingError("Could not determine image dimensions", "sharp");
682
+ return {
683
+ width: metadata.width,
684
+ height: metadata.height
685
+ };
686
+ } catch (error) {
687
+ if (error instanceof ProcessingError) throw error;
688
+ throw this.mapError(error, input);
689
+ }
690
+ }
691
+ async thumbnail(input, output, options = {}) {
692
+ const sharp = await this.getSharp();
693
+ try {
694
+ let pipeline = sharp(input).resize({
695
+ width: options.maxWidth,
696
+ height: options.maxHeight,
697
+ fit: options.fit || "inside",
698
+ withoutEnlargement: true
699
+ });
700
+ if (options.format) pipeline = pipeline.toFormat(options.format, { quality: options.quality });
701
+ else if (options.quality) {
702
+ const format = this.inferFormat(output);
703
+ pipeline = pipeline.toFormat(format, { quality: options.quality });
704
+ }
705
+ await pipeline.toFile(output);
706
+ } catch (error) {
707
+ throw this.mapError(error, input);
708
+ }
709
+ }
710
+ async convert(input, output, options = {}) {
711
+ const sharp = await this.getSharp();
712
+ try {
713
+ const format = options.format || this.inferFormat(output);
714
+ await sharp(input).toFormat(format, { quality: options.quality }).toFile(output);
715
+ } catch (error) {
716
+ throw this.mapError(error, input);
717
+ }
718
+ }
719
+ async getMetadata(input) {
720
+ const sharp = await this.getSharp();
721
+ try {
722
+ const meta = await sharp(input).metadata();
723
+ if (!meta.width || !meta.height || !meta.format) throw new ProcessingError("Could not extract image metadata", "sharp");
724
+ return {
725
+ width: meta.width,
726
+ height: meta.height,
727
+ format: meta.format,
728
+ space: meta.space,
729
+ channels: meta.channels,
730
+ depth: meta.depth,
731
+ density: meta.density,
732
+ hasAlpha: meta.hasAlpha,
733
+ orientation: meta.orientation,
734
+ exif: meta.exif ? this.parseExifBuffer(meta.exif) : void 0,
735
+ iptc: meta.iptc ? this.parseIptcBuffer(meta.iptc) : void 0,
736
+ xmp: meta.xmp ? this.parseXmpBuffer(meta.xmp) : void 0
737
+ };
738
+ } catch (error) {
739
+ if (error instanceof ProcessingError) throw error;
740
+ throw this.mapError(error, input);
741
+ }
742
+ }
743
+ async hash(input, algorithm = "perceptual") {
744
+ const sharp = await this.getSharp();
745
+ try {
746
+ if (algorithm === "md5" || algorithm === "sha256") {
747
+ const { createHash } = await import("node:crypto");
748
+ const buffer = await sharp(input).toBuffer();
749
+ return createHash(algorithm).update(buffer).digest("hex");
750
+ }
751
+ const { data } = await sharp(input).resize(8, 8, { fit: "fill" }).grayscale().raw().toBuffer({ resolveWithObject: true });
752
+ const avg = data.reduce((sum, val) => sum + val, 0) / data.length;
753
+ let hash = "";
754
+ for (const val of data) hash += val >= avg ? "1" : "0";
755
+ return BigInt(`0b${hash}`).toString(16).padStart(16, "0");
756
+ } catch (error) {
757
+ throw this.mapError(error, input);
758
+ }
759
+ }
760
+ async resize(input, output, options) {
761
+ const sharp = await this.getSharp();
762
+ try {
763
+ let pipeline = sharp(input).resize({
764
+ width: options.width,
765
+ height: options.height,
766
+ fit: options.fit || "cover"
767
+ });
768
+ if (options.format) pipeline = pipeline.toFormat(options.format, { quality: options.quality });
769
+ else if (options.quality) {
770
+ const format = this.inferFormat(output);
771
+ pipeline = pipeline.toFormat(format, { quality: options.quality });
772
+ }
773
+ await pipeline.toFile(output);
774
+ } catch (error) {
775
+ throw this.mapError(error, input);
776
+ }
777
+ }
778
+ /**
779
+ * Infer image format from file extension
780
+ */
781
+ inferFormat(path) {
782
+ return {
783
+ jpg: "jpeg",
784
+ jpeg: "jpeg",
785
+ png: "png",
786
+ webp: "webp",
787
+ avif: "avif",
788
+ gif: "gif",
789
+ tiff: "tiff",
790
+ tif: "tiff"
791
+ }[path.split(".").pop()?.toLowerCase() || ""] || "jpeg";
792
+ }
793
+ /**
794
+ * Parse EXIF buffer to object (basic parsing)
795
+ */
796
+ parseExifBuffer(buffer) {
797
+ return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
798
+ }
799
+ /**
800
+ * Parse IPTC buffer to object (basic parsing)
801
+ */
802
+ parseIptcBuffer(buffer) {
803
+ return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
804
+ }
805
+ /**
806
+ * Parse XMP buffer to object (basic parsing)
807
+ */
808
+ parseXmpBuffer(buffer) {
809
+ try {
810
+ return { raw: `${buffer.toString("utf8").slice(0, 500)}...` };
811
+ } catch {
812
+ return { raw: `${buffer.toString("base64").slice(0, 100)}...` };
813
+ }
814
+ }
815
+ /**
816
+ * Map sharp errors to our error types
817
+ */
818
+ mapError(error, input) {
819
+ const message = error instanceof Error ? error.message : String(error);
820
+ if (message.includes("Input file is missing")) throw new ImageNotFoundError(typeof input === "string" ? input : "<buffer>", "sharp");
821
+ if (message.includes("Input buffer contains unsupported image format")) throw new ProcessingError("Unsupported image format", "sharp");
822
+ return new ProcessingError(`Image processing failed: ${message}`, "sharp", error instanceof Error ? error : void 0);
823
+ }
824
+ };
825
+ //#endregion
826
+ //#region src/headline-card.ts
827
+ /**
828
+ * Cached font data
829
+ */
830
+ var cachedFontData = null;
831
+ var cachedFontName = null;
832
+ /**
833
+ * Load font from Google Fonts
834
+ *
835
+ * Note: Satori requires TTF/OTF/WOFF format, NOT WOFF2.
836
+ * We use an old browser User-Agent to get WOFF format from Google Fonts.
837
+ */
821
838
  async function loadGoogleFont(fontName) {
822
- if (cachedFontData && cachedFontName === fontName) {
823
- return cachedFontData;
824
- }
825
- const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@400;600;700&display=swap`;
826
- const cssResponse = await fetch(fontUrl, {
827
- headers: {
828
- // Use an old user agent to get WOFF format (Satori doesn't support woff2)
829
- "User-Agent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)"
830
- }
831
- });
832
- if (!cssResponse.ok) {
833
- throw new Error(`Failed to fetch font CSS: ${cssResponse.statusText}`);
834
- }
835
- const css = await cssResponse.text();
836
- let urlMatch = css.match(
837
- /src:\s*url\(([^)]+)\)\s*format\(['"]truetype['"]\)/
838
- );
839
- if (!urlMatch) {
840
- urlMatch = css.match(/src:\s*url\(([^)]+)\)\s*format\(['"]woff['"]\)/);
841
- }
842
- if (!urlMatch) {
843
- urlMatch = css.match(/src:\s*url\(([^)]+)\)(?!\s*format\(['"]woff2['"]\))/);
844
- }
845
- if (!urlMatch) {
846
- throw new Error(
847
- `Could not find compatible font URL in CSS for ${fontName}. CSS: ${css.slice(0, 300)}...`
848
- );
849
- }
850
- const fontFileUrl = urlMatch[1];
851
- const fontResponse = await fetch(fontFileUrl);
852
- if (!fontResponse.ok) {
853
- throw new Error(`Failed to fetch font file: ${fontResponse.statusText}`);
854
- }
855
- cachedFontData = await fontResponse.arrayBuffer();
856
- cachedFontName = fontName;
857
- return cachedFontData;
839
+ if (cachedFontData && cachedFontName === fontName) return cachedFontData;
840
+ const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@400;600;700&display=swap`;
841
+ const cssResponse = await fetch(fontUrl, { headers: { "User-Agent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)" } });
842
+ if (!cssResponse.ok) throw new Error(`Failed to fetch font CSS: ${cssResponse.statusText}`);
843
+ const css = await cssResponse.text();
844
+ let urlMatch = css.match(/src:\s*url\(([^)]+)\)\s*format\(['"]truetype['"]\)/);
845
+ if (!urlMatch) urlMatch = css.match(/src:\s*url\(([^)]+)\)\s*format\(['"]woff['"]\)/);
846
+ if (!urlMatch) urlMatch = css.match(/src:\s*url\(([^)]+)\)(?!\s*format\(['"]woff2['"]\))/);
847
+ if (!urlMatch) throw new Error(`Could not find compatible font URL in CSS for ${fontName}. CSS: ${css.slice(0, 300)}...`);
848
+ const fontFileUrl = urlMatch[1];
849
+ const fontResponse = await fetch(fontFileUrl);
850
+ if (!fontResponse.ok) throw new Error(`Failed to fetch font file: ${fontResponse.statusText}`);
851
+ cachedFontData = await fontResponse.arrayBuffer();
852
+ cachedFontName = fontName;
853
+ return cachedFontData;
858
854
  }
859
- function renderDefaultTemplate({
860
- title,
861
- options
862
- }) {
863
- const {
864
- width,
865
- brandColor,
866
- backgroundColor,
867
- subtitle,
868
- fontFamily
869
- } = options;
870
- return {
871
- type: "div",
872
- props: {
873
- style: {
874
- display: "flex",
875
- flexDirection: "column",
876
- justifyContent: "center",
877
- width: "100%",
878
- height: "100%",
879
- backgroundColor,
880
- padding: "60px",
881
- fontFamily
882
- },
883
- children: [
884
- // Top accent bar
885
- {
886
- type: "div",
887
- props: {
888
- style: {
889
- position: "absolute",
890
- top: 0,
891
- left: 0,
892
- right: 0,
893
- height: "8px",
894
- backgroundColor: brandColor
895
- }
896
- }
897
- },
898
- // Subtitle
899
- subtitle ? {
900
- type: "div",
901
- props: {
902
- style: {
903
- fontSize: "24px",
904
- fontWeight: 600,
905
- color: brandColor,
906
- marginBottom: "20px",
907
- textTransform: "uppercase",
908
- letterSpacing: "2px"
909
- },
910
- children: subtitle
911
- }
912
- } : null,
913
- // Title
914
- {
915
- type: "div",
916
- props: {
917
- style: {
918
- fontSize: Math.min(72, Math.floor(width / (title.length * 0.6))),
919
- fontWeight: 700,
920
- color: "#1e293b",
921
- lineHeight: 1.2,
922
- maxWidth: "90%"
923
- },
924
- children: title
925
- }
926
- },
927
- // Bottom accent
928
- {
929
- type: "div",
930
- props: {
931
- style: {
932
- position: "absolute",
933
- bottom: "60px",
934
- left: "60px",
935
- width: "80px",
936
- height: "4px",
937
- backgroundColor: brandColor
938
- }
939
- }
940
- }
941
- ].filter(Boolean)
942
- }
943
- };
855
+ /**
856
+ * Default template - clean with accent bar
857
+ */
858
+ function renderDefaultTemplate({ title, options }) {
859
+ const { width, height, brandColor, backgroundColor, textColor, subtitle, fontFamily } = options;
860
+ return {
861
+ type: "div",
862
+ props: {
863
+ style: {
864
+ display: "flex",
865
+ flexDirection: "column",
866
+ justifyContent: "center",
867
+ width: "100%",
868
+ height: "100%",
869
+ backgroundColor,
870
+ padding: "60px",
871
+ fontFamily
872
+ },
873
+ children: [
874
+ {
875
+ type: "div",
876
+ props: { style: {
877
+ position: "absolute",
878
+ top: 0,
879
+ left: 0,
880
+ right: 0,
881
+ height: "8px",
882
+ backgroundColor: brandColor
883
+ } }
884
+ },
885
+ subtitle ? {
886
+ type: "div",
887
+ props: {
888
+ style: {
889
+ fontSize: "24px",
890
+ fontWeight: 600,
891
+ color: brandColor,
892
+ marginBottom: "20px",
893
+ textTransform: "uppercase",
894
+ letterSpacing: "2px"
895
+ },
896
+ children: subtitle
897
+ }
898
+ } : null,
899
+ {
900
+ type: "div",
901
+ props: {
902
+ style: {
903
+ fontSize: Math.min(72, Math.floor(width / (title.length * .6))),
904
+ fontWeight: 700,
905
+ color: "#1e293b",
906
+ lineHeight: 1.2,
907
+ maxWidth: "90%"
908
+ },
909
+ children: title
910
+ }
911
+ },
912
+ {
913
+ type: "div",
914
+ props: { style: {
915
+ position: "absolute",
916
+ bottom: "60px",
917
+ left: "60px",
918
+ width: "80px",
919
+ height: "4px",
920
+ backgroundColor: brandColor
921
+ } }
922
+ }
923
+ ].filter(Boolean)
924
+ }
925
+ };
944
926
  }
927
+ /**
928
+ * News template - bold with side accent
929
+ */
945
930
  function renderNewsTemplate({ title, options }) {
946
- const {
947
- width,
948
- brandColor,
949
- backgroundColor,
950
- subtitle,
951
- fontFamily
952
- } = options;
953
- return {
954
- type: "div",
955
- props: {
956
- style: {
957
- display: "flex",
958
- flexDirection: "row",
959
- width: "100%",
960
- height: "100%",
961
- backgroundColor,
962
- fontFamily
963
- },
964
- children: [
965
- // Left accent bar
966
- {
967
- type: "div",
968
- props: {
969
- style: {
970
- width: "16px",
971
- height: "100%",
972
- backgroundColor: brandColor
973
- }
974
- }
975
- },
976
- // Content area
977
- {
978
- type: "div",
979
- props: {
980
- style: {
981
- display: "flex",
982
- flexDirection: "column",
983
- justifyContent: "center",
984
- flex: 1,
985
- padding: "60px"
986
- },
987
- children: [
988
- // Category badge (using flex with alignSelf since Satori doesn't support inline-flex)
989
- subtitle ? {
990
- type: "div",
991
- props: {
992
- style: {
993
- display: "flex",
994
- alignSelf: "flex-start",
995
- backgroundColor: brandColor,
996
- color: "#ffffff",
997
- fontSize: "18px",
998
- fontWeight: 600,
999
- padding: "8px 16px",
1000
- borderRadius: "4px",
1001
- marginBottom: "24px",
1002
- textTransform: "uppercase",
1003
- letterSpacing: "1px"
1004
- },
1005
- children: subtitle
1006
- }
1007
- } : null,
1008
- // Title
1009
- {
1010
- type: "div",
1011
- props: {
1012
- style: {
1013
- fontSize: Math.min(
1014
- 64,
1015
- Math.floor((width - 100) / (title.length * 0.5))
1016
- ),
1017
- fontWeight: 700,
1018
- color: "#0f172a",
1019
- lineHeight: 1.15
1020
- },
1021
- children: title
1022
- }
1023
- }
1024
- ].filter(Boolean)
1025
- }
1026
- }
1027
- ]
1028
- }
1029
- };
931
+ const { width, height, brandColor, backgroundColor, textColor, subtitle, fontFamily } = options;
932
+ return {
933
+ type: "div",
934
+ props: {
935
+ style: {
936
+ display: "flex",
937
+ flexDirection: "row",
938
+ width: "100%",
939
+ height: "100%",
940
+ backgroundColor,
941
+ fontFamily
942
+ },
943
+ children: [{
944
+ type: "div",
945
+ props: { style: {
946
+ width: "16px",
947
+ height: "100%",
948
+ backgroundColor: brandColor
949
+ } }
950
+ }, {
951
+ type: "div",
952
+ props: {
953
+ style: {
954
+ display: "flex",
955
+ flexDirection: "column",
956
+ justifyContent: "center",
957
+ flex: 1,
958
+ padding: "60px"
959
+ },
960
+ children: [subtitle ? {
961
+ type: "div",
962
+ props: {
963
+ style: {
964
+ display: "flex",
965
+ alignSelf: "flex-start",
966
+ backgroundColor: brandColor,
967
+ color: "#ffffff",
968
+ fontSize: "18px",
969
+ fontWeight: 600,
970
+ padding: "8px 16px",
971
+ borderRadius: "4px",
972
+ marginBottom: "24px",
973
+ textTransform: "uppercase",
974
+ letterSpacing: "1px"
975
+ },
976
+ children: subtitle
977
+ }
978
+ } : null, {
979
+ type: "div",
980
+ props: {
981
+ style: {
982
+ fontSize: Math.min(64, Math.floor((width - 100) / (title.length * .5))),
983
+ fontWeight: 700,
984
+ color: "#0f172a",
985
+ lineHeight: 1.15
986
+ },
987
+ children: title
988
+ }
989
+ }].filter(Boolean)
990
+ }
991
+ }]
992
+ }
993
+ };
1030
994
  }
1031
- function renderMinimalTemplate({
1032
- title,
1033
- options
1034
- }) {
1035
- const { width, brandColor, backgroundColor, fontFamily } = options;
1036
- return {
1037
- type: "div",
1038
- props: {
1039
- style: {
1040
- display: "flex",
1041
- flexDirection: "column",
1042
- alignItems: "center",
1043
- justifyContent: "center",
1044
- width: "100%",
1045
- height: "100%",
1046
- backgroundColor,
1047
- padding: "80px",
1048
- fontFamily
1049
- },
1050
- children: [
1051
- // Decorative line
1052
- {
1053
- type: "div",
1054
- props: {
1055
- style: {
1056
- width: "60px",
1057
- height: "3px",
1058
- backgroundColor: brandColor,
1059
- marginBottom: "40px"
1060
- }
1061
- }
1062
- },
1063
- // Title
1064
- {
1065
- type: "div",
1066
- props: {
1067
- style: {
1068
- fontSize: Math.min(56, Math.floor(width / (title.length * 0.55))),
1069
- fontWeight: 600,
1070
- color: "#334155",
1071
- lineHeight: 1.3,
1072
- textAlign: "center",
1073
- maxWidth: "85%"
1074
- },
1075
- children: title
1076
- }
1077
- },
1078
- // Decorative line
1079
- {
1080
- type: "div",
1081
- props: {
1082
- style: {
1083
- width: "60px",
1084
- height: "3px",
1085
- backgroundColor: brandColor,
1086
- marginTop: "40px"
1087
- }
1088
- }
1089
- }
1090
- ]
1091
- }
1092
- };
995
+ /**
996
+ * Minimal template - simple and elegant
997
+ */
998
+ function renderMinimalTemplate({ title, options }) {
999
+ const { width, height, brandColor, backgroundColor, textColor, fontFamily } = options;
1000
+ return {
1001
+ type: "div",
1002
+ props: {
1003
+ style: {
1004
+ display: "flex",
1005
+ flexDirection: "column",
1006
+ alignItems: "center",
1007
+ justifyContent: "center",
1008
+ width: "100%",
1009
+ height: "100%",
1010
+ backgroundColor,
1011
+ padding: "80px",
1012
+ fontFamily
1013
+ },
1014
+ children: [
1015
+ {
1016
+ type: "div",
1017
+ props: { style: {
1018
+ width: "60px",
1019
+ height: "3px",
1020
+ backgroundColor: brandColor,
1021
+ marginBottom: "40px"
1022
+ } }
1023
+ },
1024
+ {
1025
+ type: "div",
1026
+ props: {
1027
+ style: {
1028
+ fontSize: Math.min(56, Math.floor(width / (title.length * .55))),
1029
+ fontWeight: 600,
1030
+ color: "#334155",
1031
+ lineHeight: 1.3,
1032
+ textAlign: "center",
1033
+ maxWidth: "85%"
1034
+ },
1035
+ children: title
1036
+ }
1037
+ },
1038
+ {
1039
+ type: "div",
1040
+ props: { style: {
1041
+ width: "60px",
1042
+ height: "3px",
1043
+ backgroundColor: brandColor,
1044
+ marginTop: "40px"
1045
+ } }
1046
+ }
1047
+ ]
1048
+ }
1049
+ };
1093
1050
  }
1051
+ /**
1052
+ * Generate a headline card image
1053
+ *
1054
+ * Creates a branded social media / OG image with the article title.
1055
+ * Supports multiple templates and customization options.
1056
+ *
1057
+ * @param title - The headline text to display
1058
+ * @param options - Customization options
1059
+ * @returns Promise resolving to PNG image result
1060
+ *
1061
+ * @example Basic usage
1062
+ * ```typescript
1063
+ * const result = await generateHeadlineCard('Breaking News: AI Advances');
1064
+ * await fs.writeFile('og-image.png', result.buffer);
1065
+ * ```
1066
+ *
1067
+ * @example With branding
1068
+ * ```typescript
1069
+ * const result = await generateHeadlineCard('Local Council Approves Budget', {
1070
+ * brandColor: '#1a56db',
1071
+ * subtitle: 'Town News',
1072
+ * template: 'news'
1073
+ * });
1074
+ * ```
1075
+ *
1076
+ * @example Full customization
1077
+ * ```typescript
1078
+ * const result = await generateHeadlineCard('The Future of Technology', {
1079
+ * width: 1200,
1080
+ * height: 630,
1081
+ * brandColor: '#059669',
1082
+ * backgroundColor: '#f8fafc',
1083
+ * subtitle: 'Technology',
1084
+ * template: 'minimal',
1085
+ * fontFamily: 'Roboto'
1086
+ * });
1087
+ * ```
1088
+ */
1094
1089
  async function generateHeadlineCard(title, options = {}) {
1095
- const width = options.width ?? 1200;
1096
- const height = options.height ?? 630;
1097
- const brandColor = options.brandColor ?? "#3b82f6";
1098
- const backgroundColor = options.backgroundColor ?? "#ffffff";
1099
- options.textColor ?? "#64748b";
1100
- const template = options.template ?? "default";
1101
- const fontFamily = options.fontFamily ?? "Inter";
1102
- const resolvedOptions = {
1103
- ...options,
1104
- width,
1105
- brandColor,
1106
- backgroundColor,
1107
- fontFamily
1108
- };
1109
- const [{ default: satori }, { Resvg }] = await Promise.all([
1110
- import("satori"),
1111
- import("@resvg/resvg-js")
1112
- ]);
1113
- const fontData = options.fontData ?? await loadGoogleFont(fontFamily);
1114
- let content;
1115
- switch (template) {
1116
- case "news":
1117
- content = renderNewsTemplate({ title, options: resolvedOptions });
1118
- break;
1119
- case "minimal":
1120
- content = renderMinimalTemplate({ title, options: resolvedOptions });
1121
- break;
1122
- default:
1123
- content = renderDefaultTemplate({ title, options: resolvedOptions });
1124
- }
1125
- const svg = await satori(content, {
1126
- width,
1127
- height,
1128
- fonts: [
1129
- {
1130
- name: fontFamily,
1131
- data: fontData,
1132
- weight: 400,
1133
- style: "normal"
1134
- },
1135
- {
1136
- name: fontFamily,
1137
- data: fontData,
1138
- weight: 600,
1139
- style: "normal"
1140
- },
1141
- {
1142
- name: fontFamily,
1143
- data: fontData,
1144
- weight: 700,
1145
- style: "normal"
1146
- }
1147
- ]
1148
- });
1149
- const resvg = new Resvg(svg, {
1150
- fitTo: {
1151
- mode: "width",
1152
- value: width
1153
- }
1154
- });
1155
- const pngData = resvg.render();
1156
- const buffer = Buffer.from(pngData.asPng());
1157
- return {
1158
- buffer,
1159
- width,
1160
- height,
1161
- mimeType: "image/png"
1162
- };
1090
+ const width = options.width ?? 1200;
1091
+ const height = options.height ?? 630;
1092
+ const brandColor = options.brandColor ?? "#3b82f6";
1093
+ const backgroundColor = options.backgroundColor ?? "#ffffff";
1094
+ const textColor = options.textColor ?? "#64748b";
1095
+ const template = options.template ?? "default";
1096
+ const fontFamily = options.fontFamily ?? "Inter";
1097
+ const resolvedOptions = {
1098
+ ...options,
1099
+ width,
1100
+ height,
1101
+ brandColor,
1102
+ backgroundColor,
1103
+ textColor,
1104
+ fontFamily
1105
+ };
1106
+ const [{ default: satori }, { Resvg }] = await Promise.all([import("satori"), import("@resvg/resvg-js")]);
1107
+ const fontData = options.fontData ?? await loadGoogleFont(fontFamily);
1108
+ let content;
1109
+ switch (template) {
1110
+ case "news":
1111
+ content = renderNewsTemplate({
1112
+ title,
1113
+ options: resolvedOptions
1114
+ });
1115
+ break;
1116
+ case "minimal":
1117
+ content = renderMinimalTemplate({
1118
+ title,
1119
+ options: resolvedOptions
1120
+ });
1121
+ break;
1122
+ default: content = renderDefaultTemplate({
1123
+ title,
1124
+ options: resolvedOptions
1125
+ });
1126
+ }
1127
+ const pngData = new Resvg(await satori(content, {
1128
+ width,
1129
+ height,
1130
+ fonts: [
1131
+ {
1132
+ name: fontFamily,
1133
+ data: fontData,
1134
+ weight: 400,
1135
+ style: "normal"
1136
+ },
1137
+ {
1138
+ name: fontFamily,
1139
+ data: fontData,
1140
+ weight: 600,
1141
+ style: "normal"
1142
+ },
1143
+ {
1144
+ name: fontFamily,
1145
+ data: fontData,
1146
+ weight: 700,
1147
+ style: "normal"
1148
+ }
1149
+ ]
1150
+ }), { fitTo: {
1151
+ mode: "width",
1152
+ value: width
1153
+ } }).render();
1154
+ return {
1155
+ buffer: Buffer.from(pngData.asPng()),
1156
+ width,
1157
+ height,
1158
+ mimeType: "image/png"
1159
+ };
1163
1160
  }
1161
+ /**
1162
+ * Reset cached font data (useful for testing)
1163
+ */
1164
1164
  function resetFontCache() {
1165
- cachedFontData = null;
1166
- cachedFontName = null;
1165
+ cachedFontData = null;
1166
+ cachedFontName = null;
1167
1167
  }
1168
+ //#endregion
1169
+ //#region src/shared/factory.ts
1170
+ /**
1171
+ * Factory function for creating image processor instances
1172
+ */
1173
+ var factory_exports = /* @__PURE__ */ __exportAll({
1174
+ getAvailableAdapters: () => getAvailableAdapters,
1175
+ getImageProcessor: () => getImageProcessor
1176
+ });
1177
+ /**
1178
+ * Type guards for processor options
1179
+ */
1180
+ /**
1181
+ * Checks if the options are for Sharp adapter
1182
+ */
1168
1183
  function isSharpOptions(options) {
1169
- return !options.type || options.type === "sharp";
1184
+ return !options.type || options.type === "sharp";
1170
1185
  }
1186
+ /**
1187
+ * Checks if the options are for Jimp adapter
1188
+ */
1171
1189
  function isJimpOptions(options) {
1172
- return options.type === "jimp";
1190
+ return options.type === "jimp";
1173
1191
  }
1192
+ /**
1193
+ * Checks if the options are for imgproxy adapter
1194
+ */
1174
1195
  function isImgproxyOptions(options) {
1175
- return options.type === "imgproxy";
1196
+ return options.type === "imgproxy";
1176
1197
  }
1198
+ /**
1199
+ * Load environment variables for configuration
1200
+ * Pattern: HAVE_IMAGES_*
1201
+ */
1177
1202
  function loadEnvConfig(options) {
1178
- if (typeof process === "undefined" || !process.env) {
1179
- return options;
1180
- }
1181
- const env = process.env;
1182
- const envDefaults = {};
1183
- const typeEnv = env.HAVE_IMAGES_TYPE || env.HAVE_IMAGES_ADAPTER;
1184
- if (typeEnv && !options.type) {
1185
- envDefaults.type = typeEnv;
1186
- }
1187
- if (env.HAVE_IMAGES_BASE_URL) {
1188
- envDefaults.baseUrl = env.HAVE_IMAGES_BASE_URL;
1189
- }
1190
- if (env.HAVE_IMAGES_KEY) {
1191
- envDefaults.key = env.HAVE_IMAGES_KEY;
1192
- }
1193
- if (env.HAVE_IMAGES_SALT) {
1194
- envDefaults.salt = env.HAVE_IMAGES_SALT;
1195
- }
1196
- return { ...envDefaults, ...options };
1203
+ if (typeof process === "undefined" || !process.env) return options;
1204
+ const env = process.env;
1205
+ const envDefaults = {};
1206
+ const typeEnv = env.HAVE_IMAGES_TYPE || env.HAVE_IMAGES_ADAPTER;
1207
+ if (typeEnv && !options.type) envDefaults.type = typeEnv;
1208
+ if (env.HAVE_IMAGES_BASE_URL) envDefaults.baseUrl = env.HAVE_IMAGES_BASE_URL;
1209
+ if (env.HAVE_IMAGES_KEY) envDefaults.key = env.HAVE_IMAGES_KEY;
1210
+ if (env.HAVE_IMAGES_SALT) envDefaults.salt = env.HAVE_IMAGES_SALT;
1211
+ return {
1212
+ ...envDefaults,
1213
+ ...options
1214
+ };
1197
1215
  }
1216
+ /**
1217
+ * Creates an image processor instance based on the provided options.
1218
+ *
1219
+ * Supports environment variable configuration using the pattern:
1220
+ * - HAVE_IMAGES_TYPE → adapter type ('sharp', 'jimp', 'imgproxy')
1221
+ * - HAVE_IMAGES_BASE_URL → imgproxy base URL
1222
+ * - HAVE_IMAGES_KEY → imgproxy signing key (hex)
1223
+ * - HAVE_IMAGES_SALT → imgproxy signing salt (hex)
1224
+ *
1225
+ * User-provided options always take precedence over environment variables.
1226
+ *
1227
+ * @param options - Configuration options for the image processor
1228
+ * @returns Promise resolving to an image processor instance
1229
+ * @throws {InvalidAdapterError} When the adapter type is unsupported
1230
+ *
1231
+ * @example
1232
+ * ```typescript
1233
+ * // Create Sharp processor (default)
1234
+ * const processor = await getImageProcessor();
1235
+ *
1236
+ * // Create Jimp processor
1237
+ * const jimpProcessor = await getImageProcessor({ type: 'jimp' });
1238
+ *
1239
+ * // Create imgproxy processor
1240
+ * const imgproxyProcessor = await getImageProcessor({
1241
+ * type: 'imgproxy',
1242
+ * baseUrl: 'https://imgproxy.example.com',
1243
+ * key: 'abcd1234...',
1244
+ * salt: 'efgh5678...'
1245
+ * });
1246
+ *
1247
+ * // Use environment variables (HAVE_IMAGES_TYPE=sharp)
1248
+ * const autoProcessor = await getImageProcessor({});
1249
+ * ```
1250
+ */
1198
1251
  async function getImageProcessor(options = {}) {
1199
- options = loadEnvConfig(options);
1200
- if (isSharpOptions(options)) {
1201
- const { SharpAdapter: SharpAdapter2 } = await Promise.resolve().then(() => sharp);
1202
- return new SharpAdapter2(options);
1203
- }
1204
- if (isJimpOptions(options)) {
1205
- const { JimpAdapter: JimpAdapter2 } = await Promise.resolve().then(() => jimp);
1206
- return new JimpAdapter2(options);
1207
- }
1208
- if (isImgproxyOptions(options)) {
1209
- const { ImgproxyAdapter: ImgproxyAdapter2 } = await Promise.resolve().then(() => imgproxy);
1210
- return new ImgproxyAdapter2(options);
1211
- }
1212
- throw new InvalidAdapterError(
1213
- options.type || "unknown"
1214
- );
1252
+ options = loadEnvConfig(options);
1253
+ if (isSharpOptions(options)) {
1254
+ const { SharpAdapter } = await Promise.resolve().then(() => sharp_exports);
1255
+ return new SharpAdapter(options);
1256
+ }
1257
+ if (isJimpOptions(options)) {
1258
+ const { JimpAdapter } = await Promise.resolve().then(() => jimp_exports);
1259
+ return new JimpAdapter(options);
1260
+ }
1261
+ if (isImgproxyOptions(options)) {
1262
+ const { ImgproxyAdapter } = await Promise.resolve().then(() => imgproxy_exports);
1263
+ return new ImgproxyAdapter(options);
1264
+ }
1265
+ throw new InvalidAdapterError(options.type || "unknown");
1215
1266
  }
1267
+ /**
1268
+ * Get a list of available adapter types
1269
+ */
1216
1270
  function getAvailableAdapters() {
1217
- return ["sharp", "jimp", "imgproxy"];
1271
+ return [
1272
+ "sharp",
1273
+ "jimp",
1274
+ "imgproxy"
1275
+ ];
1218
1276
  }
1219
- const factory = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1220
- __proto__: null,
1221
- getAvailableAdapters,
1222
- getImageProcessor
1223
- }, Symbol.toStringTag, { value: "Module" }));
1224
- let cachedProcessor = null;
1225
- let detectionAttempted = false;
1277
+ //#endregion
1278
+ //#region src/index.ts
1279
+ /**
1280
+ * Cached processor instance for auto-detection
1281
+ */
1282
+ var cachedProcessor = null;
1283
+ var detectionAttempted = false;
1284
+ /**
1285
+ * Get a default processor with auto-detection
1286
+ *
1287
+ * Auto-detection priority:
1288
+ * 1. If explicit adapter options provided, use those
1289
+ * 2. Try to load Sharp (fast native processing)
1290
+ * 3. Fall back to Jimp (pure JavaScript)
1291
+ *
1292
+ * The processor is cached for subsequent calls to avoid repeated detection.
1293
+ */
1226
1294
  async function getDefaultProcessor(adapterOptions) {
1227
- if (adapterOptions?.type) {
1228
- const { getImageProcessor: getImageProcessor2 } = await Promise.resolve().then(() => factory);
1229
- return getImageProcessor2(adapterOptions);
1230
- }
1231
- if (cachedProcessor && !adapterOptions) {
1232
- return cachedProcessor;
1233
- }
1234
- if (!detectionAttempted) {
1235
- detectionAttempted = true;
1236
- try {
1237
- await import("sharp");
1238
- const { SharpAdapter: SharpAdapter2 } = await Promise.resolve().then(() => sharp);
1239
- cachedProcessor = new SharpAdapter2({});
1240
- } catch {
1241
- const { JimpAdapter: JimpAdapter2 } = await Promise.resolve().then(() => jimp);
1242
- cachedProcessor = new JimpAdapter2({ type: "jimp" });
1243
- }
1244
- }
1245
- if (!cachedProcessor) {
1246
- throw new Error("No image processor available. Install sharp or jimp.");
1247
- }
1248
- return cachedProcessor;
1295
+ if (adapterOptions?.type) {
1296
+ const { getImageProcessor } = await Promise.resolve().then(() => factory_exports);
1297
+ return getImageProcessor(adapterOptions);
1298
+ }
1299
+ if (cachedProcessor && !adapterOptions) return cachedProcessor;
1300
+ if (!detectionAttempted) {
1301
+ detectionAttempted = true;
1302
+ try {
1303
+ await import("sharp");
1304
+ const { SharpAdapter } = await Promise.resolve().then(() => sharp_exports);
1305
+ cachedProcessor = new SharpAdapter({});
1306
+ } catch {
1307
+ const { JimpAdapter } = await Promise.resolve().then(() => jimp_exports);
1308
+ cachedProcessor = new JimpAdapter({ type: "jimp" });
1309
+ }
1310
+ }
1311
+ if (!cachedProcessor) throw new Error("No image processor available. Install sharp or jimp.");
1312
+ return cachedProcessor;
1249
1313
  }
1314
+ /**
1315
+ * Reset the cached processor (useful for testing)
1316
+ */
1250
1317
  function resetProcessor() {
1251
- cachedProcessor = null;
1252
- detectionAttempted = false;
1318
+ cachedProcessor = null;
1319
+ detectionAttempted = false;
1253
1320
  }
1321
+ /**
1322
+ * Get image dimensions
1323
+ *
1324
+ * Uses auto-detection to select the best available processor.
1325
+ *
1326
+ * @param input - Image path or Buffer
1327
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1328
+ * @returns Promise resolving to image dimensions
1329
+ *
1330
+ * @example
1331
+ * ```typescript
1332
+ * import { getDimensions } from '@happyvertical/images';
1333
+ *
1334
+ * // Auto-detect adapter
1335
+ * const dims = await getDimensions('/path/to/image.jpg');
1336
+ * console.log(`${dims.width}x${dims.height}`);
1337
+ *
1338
+ * // Force specific adapter
1339
+ * const jimp = await getDimensions(buffer, { type: 'jimp' });
1340
+ * ```
1341
+ */
1254
1342
  async function getDimensions(input, adapterOptions) {
1255
- const processor = await getDefaultProcessor(adapterOptions);
1256
- return processor.getDimensions(input);
1343
+ return (await getDefaultProcessor(adapterOptions)).getDimensions(input);
1257
1344
  }
1345
+ /**
1346
+ * Generate a thumbnail image
1347
+ *
1348
+ * Creates a smaller version of the image while maintaining aspect ratio.
1349
+ * Uses auto-detection to select the best available processor.
1350
+ *
1351
+ * @param input - Source image path or Buffer
1352
+ * @param output - Output file path
1353
+ * @param options - Thumbnail options (maxWidth, maxHeight, quality, format)
1354
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1355
+ *
1356
+ * @example
1357
+ * ```typescript
1358
+ * import { generateThumbnail } from '@happyvertical/images';
1359
+ *
1360
+ * // Create 300x300 max thumbnail
1361
+ * await generateThumbnail('/path/to/image.jpg', '/path/to/thumb.jpg', {
1362
+ * maxWidth: 300,
1363
+ * maxHeight: 300,
1364
+ * quality: 85
1365
+ * });
1366
+ *
1367
+ * // Convert to WebP while thumbnailing
1368
+ * await generateThumbnail('/path/to/image.jpg', '/path/to/thumb.webp', {
1369
+ * maxWidth: 200,
1370
+ * format: 'webp'
1371
+ * });
1372
+ * ```
1373
+ */
1258
1374
  async function generateThumbnail(input, output, options, adapterOptions) {
1259
- const processor = await getDefaultProcessor(adapterOptions);
1260
- return processor.thumbnail(input, output, options);
1375
+ return (await getDefaultProcessor(adapterOptions)).thumbnail(input, output, options);
1261
1376
  }
1377
+ /**
1378
+ * Convert image format
1379
+ *
1380
+ * Converts an image to a different format with optional quality settings.
1381
+ * Uses auto-detection to select the best available processor.
1382
+ *
1383
+ * @param input - Source image path or Buffer
1384
+ * @param output - Output file path (format inferred from extension)
1385
+ * @param options - Conversion options (format, quality)
1386
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1387
+ *
1388
+ * @example
1389
+ * ```typescript
1390
+ * import { convertFormat } from '@happyvertical/images';
1391
+ *
1392
+ * // Convert PNG to JPEG
1393
+ * await convertFormat('/path/to/image.png', '/path/to/image.jpg', {
1394
+ * quality: 90
1395
+ * });
1396
+ *
1397
+ * // Convert to WebP with explicit format
1398
+ * await convertFormat('/path/to/image.jpg', '/path/to/image.webp', {
1399
+ * format: 'webp',
1400
+ * quality: 80
1401
+ * });
1402
+ * ```
1403
+ */
1262
1404
  async function convertFormat(input, output, options, adapterOptions) {
1263
- const processor = await getDefaultProcessor(adapterOptions);
1264
- return processor.convert(input, output, options);
1405
+ return (await getDefaultProcessor(adapterOptions)).convert(input, output, options);
1265
1406
  }
1407
+ /**
1408
+ * Get image metadata
1409
+ *
1410
+ * Extracts comprehensive metadata including dimensions, format, color space,
1411
+ * and optionally EXIF/IPTC/XMP data.
1412
+ * Uses auto-detection to select the best available processor.
1413
+ *
1414
+ * @param input - Image path or Buffer
1415
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1416
+ * @returns Promise resolving to metadata object
1417
+ *
1418
+ * @example
1419
+ * ```typescript
1420
+ * import { getImageMetadata } from '@happyvertical/images';
1421
+ *
1422
+ * const metadata = await getImageMetadata('/path/to/photo.jpg');
1423
+ * console.log(`Format: ${metadata.format}`);
1424
+ * console.log(`Dimensions: ${metadata.width}x${metadata.height}`);
1425
+ * console.log(`Has alpha: ${metadata.hasAlpha}`);
1426
+ * if (metadata.exif) {
1427
+ * console.log('EXIF data:', metadata.exif);
1428
+ * }
1429
+ * ```
1430
+ */
1266
1431
  async function getImageMetadata(input, adapterOptions) {
1267
- const processor = await getDefaultProcessor(adapterOptions);
1268
- return processor.getMetadata(input);
1432
+ return (await getDefaultProcessor(adapterOptions)).getMetadata(input);
1269
1433
  }
1434
+ /**
1435
+ * Compute image hash
1436
+ *
1437
+ * Generates a hash of the image for deduplication or comparison.
1438
+ * Uses auto-detection to select the best available processor.
1439
+ *
1440
+ * @param input - Image path or Buffer
1441
+ * @param algorithm - Hash algorithm: 'perceptual' (default), 'md5', 'sha256'
1442
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1443
+ * @returns Promise resolving to hash string
1444
+ *
1445
+ * @example
1446
+ * ```typescript
1447
+ * import { getImageHash } from '@happyvertical/images';
1448
+ *
1449
+ * // Perceptual hash for finding similar images
1450
+ * const pHash = await getImageHash('/path/to/image.jpg');
1451
+ * console.log(`Perceptual hash: ${pHash}`);
1452
+ *
1453
+ * // Cryptographic hash for exact matching
1454
+ * const sha256 = await getImageHash('/path/to/image.jpg', 'sha256');
1455
+ * console.log(`SHA-256: ${sha256}`);
1456
+ * ```
1457
+ */
1270
1458
  async function getImageHash(input, algorithm = "perceptual", adapterOptions) {
1271
- const processor = await getDefaultProcessor(adapterOptions);
1272
- return processor.hash(input, algorithm);
1459
+ return (await getDefaultProcessor(adapterOptions)).hash(input, algorithm);
1273
1460
  }
1461
+ /**
1462
+ * Resize image with full control
1463
+ *
1464
+ * Resizes an image to specific dimensions with control over fit mode.
1465
+ * Uses auto-detection to select the best available processor.
1466
+ *
1467
+ * @param input - Source image path or Buffer
1468
+ * @param output - Output file path
1469
+ * @param options - Resize options (width, height, fit, quality, format)
1470
+ * @param adapterOptions - Optional adapter configuration to override auto-detection
1471
+ *
1472
+ * @example
1473
+ * ```typescript
1474
+ * import { resizeImage } from '@happyvertical/images';
1475
+ *
1476
+ * // Resize to exact dimensions (may crop)
1477
+ * await resizeImage('/path/to/image.jpg', '/path/to/resized.jpg', {
1478
+ * width: 800,
1479
+ * height: 600,
1480
+ * fit: 'cover' // Crop to fill
1481
+ * });
1482
+ *
1483
+ * // Resize to fit within dimensions (no crop)
1484
+ * await resizeImage('/path/to/image.jpg', '/path/to/resized.jpg', {
1485
+ * width: 800,
1486
+ * height: 600,
1487
+ * fit: 'inside' // Fit within, may be smaller
1488
+ * });
1489
+ * ```
1490
+ */
1274
1491
  async function resizeImage(input, output, options, adapterOptions) {
1275
- const processor = await getDefaultProcessor(adapterOptions);
1276
- return processor.resize(input, output, options);
1492
+ return (await getDefaultProcessor(adapterOptions)).resize(input, output, options);
1277
1493
  }
1278
- export {
1279
- ImageError,
1280
- ImageNotFoundError,
1281
- ImgproxyAdapter,
1282
- InvalidAdapterError,
1283
- JimpAdapter,
1284
- OperationNotSupportedError,
1285
- ProcessingError,
1286
- RemoteServiceError,
1287
- SharpAdapter,
1288
- UnsupportedFormatError,
1289
- convertFormat,
1290
- generateHeadlineCard,
1291
- generateThumbnail,
1292
- getAvailableAdapters,
1293
- getDimensions,
1294
- getImageHash,
1295
- getImageMetadata,
1296
- getImageProcessor,
1297
- resetFontCache,
1298
- resetProcessor,
1299
- resizeImage,
1300
- signImgproxyUrl
1301
- };
1302
- //# sourceMappingURL=index.js.map
1494
+ //#endregion
1495
+ export { ImageError, ImageNotFoundError, ImgproxyAdapter, InvalidAdapterError, JimpAdapter, OperationNotSupportedError, ProcessingError, RemoteServiceError, SharpAdapter, UnsupportedFormatError, convertFormat, generateHeadlineCard, generateThumbnail, getAvailableAdapters, getDimensions, getImageHash, getImageMetadata, getImageProcessor, resetFontCache, resetProcessor, resizeImage, signImgproxyUrl };
1496
+
1497
+ //# sourceMappingURL=index.js.map