ipx 0.9.0 → 0.9.4

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/README.md CHANGED
@@ -52,27 +52,29 @@ Resize to `200x200px` using `embed` method and change format to `webp`:
52
52
 
53
53
  ### Modifiers
54
54
 
55
- | Property | Docs | Example | Comments |
56
- | ----------- | :-------------------------------------------------------------- | :------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | width / w | \_ | `http://localhost:3000/width_200/buffalo.png` |
58
- | height / h | \_ | `http://localhost:3000/height_200/buffalo.png` |
59
- | resize / s | \_ | `http://localhost:3000/s_200x200/buffalo.png` |
60
- | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
61
- | format | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `http://localhost:3000/format_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif` |
62
- | quality / q | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
63
- | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
64
- | enlarge | \_ | `http://localhost:3000/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
65
- | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
66
- | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
67
- | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
68
- | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
69
- | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
70
- | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
71
- | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
72
- | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
73
- | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
74
- | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
75
- | animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
55
+ | Property | Docs | Example | Comments |
56
+ | --------------- | :-------------------------------------------------------------- | :---------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | width / w | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/width_200/buffalo.png` |
58
+ | height / h | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/height_200/buffalo.png` |
59
+ | resize / s | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200/buffalo.png` |
60
+ | fit | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200,fit_outside/buffalo.png` | Sets `fit` option for `resize`.
61
+ | position / pos | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `http://localhost:3000/s_200x200,pos_top/buffalo.png` | Sets `position` option for `resize`.
62
+ | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
63
+ | format | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `http://localhost:3000/format_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif` |
64
+ | quality / q | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
65
+ | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
66
+ | enlarge | \_ | `http://localhost:3000/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
67
+ | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
68
+ | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
69
+ | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
70
+ | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
71
+ | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
72
+ | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
73
+ | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
74
+ | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
75
+ | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
76
+ | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
77
+ | animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
76
78
 
77
79
  ### Config
78
80
 
package/bin/ipx.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs'
@@ -0,0 +1,558 @@
1
+ 'use strict';
2
+
3
+ const defu = require('defu');
4
+ const imageMeta = require('image-meta');
5
+ const ufo = require('ufo');
6
+ const fs = require('fs');
7
+ const pathe = require('pathe');
8
+ const isValidPath = require('is-valid-path');
9
+ const http = require('http');
10
+ const https = require('https');
11
+ const ohmyfetch = require('ohmyfetch');
12
+ const destr = require('destr');
13
+ const getEtag = require('etag');
14
+ const xss = require('xss');
15
+
16
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
17
+
18
+ const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
19
+ const isValidPath__default = /*#__PURE__*/_interopDefaultLegacy(isValidPath);
20
+ const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
21
+ const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
22
+ const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
23
+ const getEtag__default = /*#__PURE__*/_interopDefaultLegacy(getEtag);
24
+ const xss__default = /*#__PURE__*/_interopDefaultLegacy(xss);
25
+
26
+ const Handlers = {
27
+ __proto__: null,
28
+ get quality () { return quality; },
29
+ get fit () { return fit; },
30
+ get position () { return position; },
31
+ get background () { return background; },
32
+ get enlarge () { return enlarge; },
33
+ get width () { return width; },
34
+ get height () { return height; },
35
+ get resize () { return resize; },
36
+ get trim () { return trim; },
37
+ get extend () { return extend; },
38
+ get extract () { return extract; },
39
+ get rotate () { return rotate; },
40
+ get flip () { return flip; },
41
+ get flop () { return flop; },
42
+ get sharpen () { return sharpen; },
43
+ get median () { return median; },
44
+ get blur () { return blur; },
45
+ get flatten () { return flatten; },
46
+ get gamma () { return gamma; },
47
+ get negate () { return negate; },
48
+ get normalize () { return normalize; },
49
+ get threshold () { return threshold; },
50
+ get modulate () { return modulate; },
51
+ get tint () { return tint; },
52
+ get grayscale () { return grayscale; },
53
+ get crop () { return crop; },
54
+ get q () { return q; },
55
+ get b () { return b; },
56
+ get w () { return w; },
57
+ get h () { return h; },
58
+ get s () { return s; },
59
+ get pos () { return pos; }
60
+ };
61
+
62
+ function getEnv(name, defaultValue) {
63
+ return destr__default(process.env[name]) ?? defaultValue;
64
+ }
65
+ function cachedPromise(fn) {
66
+ let p;
67
+ return (...args) => {
68
+ if (p) {
69
+ return p;
70
+ }
71
+ p = Promise.resolve(fn(...args));
72
+ return p;
73
+ };
74
+ }
75
+ class IPXError extends Error {
76
+ }
77
+ function createError(message, statusCode) {
78
+ const err = new IPXError(message);
79
+ err.statusMessage = "IPX: " + message;
80
+ err.statusCode = statusCode;
81
+ return err;
82
+ }
83
+
84
+ const createFilesystemSource = (options) => {
85
+ const rootDir = pathe.resolve(options.dir);
86
+ return async (id) => {
87
+ const fsPath = pathe.resolve(pathe.join(rootDir, id));
88
+ if (!isValidPath__default(id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
89
+ throw createError("Forbidden path:" + id, 403);
90
+ }
91
+ let stats;
92
+ try {
93
+ stats = await fs.promises.stat(fsPath);
94
+ } catch (err) {
95
+ if (err.code === "ENOENT") {
96
+ throw createError("File not found: " + fsPath, 404);
97
+ } else {
98
+ throw createError("File access error for " + fsPath + ":" + err.code, 403);
99
+ }
100
+ }
101
+ if (!stats.isFile()) {
102
+ throw createError("Path should be a file: " + fsPath, 400);
103
+ }
104
+ return {
105
+ mtime: stats.mtime,
106
+ maxAge: options.maxAge || 300,
107
+ getData: cachedPromise(() => fs.promises.readFile(fsPath))
108
+ };
109
+ };
110
+ };
111
+
112
+ const createHTTPSource = (options) => {
113
+ const httpsAgent = new https__default.Agent({ keepAlive: true });
114
+ const httpAgent = new http__default.Agent({ keepAlive: true });
115
+ let domains = options.domains || [];
116
+ if (typeof domains === "string") {
117
+ domains = domains.split(",").map((s) => s.trim());
118
+ }
119
+ const hosts = domains.map((domain) => ufo.parseURL(domain, "https://").host);
120
+ return async (id, reqOptions) => {
121
+ const url = new URL(id);
122
+ if (!url.hostname) {
123
+ throw createError("Hostname is missing: " + id, 403);
124
+ }
125
+ if (!reqOptions?.bypassDomain && !hosts.find((host) => url.hostname === host)) {
126
+ throw createError("Forbidden host: " + url.hostname, 403);
127
+ }
128
+ const response = await ohmyfetch.fetch(id, {
129
+ agent: id.startsWith("https") ? httpsAgent : httpAgent
130
+ });
131
+ if (!response.ok) {
132
+ throw createError(response.statusText || "fetch error", response.status || 500);
133
+ }
134
+ let maxAge = options.maxAge || 300;
135
+ const _cacheControl = response.headers.get("cache-control");
136
+ if (_cacheControl) {
137
+ const m = _cacheControl.match(/max-age=(\d+)/);
138
+ if (m && m[1]) {
139
+ maxAge = parseInt(m[1]);
140
+ }
141
+ }
142
+ let mtime;
143
+ const _lastModified = response.headers.get("last-modified");
144
+ if (_lastModified) {
145
+ mtime = new Date(_lastModified);
146
+ }
147
+ return {
148
+ mtime,
149
+ maxAge,
150
+ getData: cachedPromise(() => response.buffer())
151
+ };
152
+ };
153
+ };
154
+
155
+ function VArg(arg) {
156
+ return destr__default(arg);
157
+ }
158
+ function parseArgs(args, mappers) {
159
+ const vargs = args.split("_");
160
+ return mappers.map((v, i) => v(vargs[i]));
161
+ }
162
+ function getHandler(key) {
163
+ return Handlers[key];
164
+ }
165
+ function applyHandler(ctx, pipe, handler, argsStr) {
166
+ const args = handler.args ? parseArgs(argsStr, handler.args) : [];
167
+ return handler.apply(ctx, pipe, ...args);
168
+ }
169
+ function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
170
+ const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
171
+ let { width, height } = desiredDimensions;
172
+ if (width > sourceDimensions.width) {
173
+ width = sourceDimensions.width;
174
+ height = Math.round(sourceDimensions.width / desiredAspectRatio);
175
+ }
176
+ if (height > sourceDimensions.height) {
177
+ height = sourceDimensions.height;
178
+ width = Math.round(sourceDimensions.height * desiredAspectRatio);
179
+ }
180
+ return { width, height };
181
+ }
182
+
183
+ const quality = {
184
+ args: [VArg],
185
+ order: -1,
186
+ apply: (context, _pipe, quality2) => {
187
+ context.quality = quality2;
188
+ }
189
+ };
190
+ const fit = {
191
+ args: [VArg],
192
+ order: -1,
193
+ apply: (context, _pipe, fit2) => {
194
+ context.fit = fit2;
195
+ }
196
+ };
197
+ const position = {
198
+ args: [VArg],
199
+ order: -1,
200
+ apply: (context, _pipe, position2) => {
201
+ context.position = position2;
202
+ }
203
+ };
204
+ const HEX_RE = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
205
+ const SHORTHEX_RE = /^([a-f\d])([a-f\d])([a-f\d])$/i;
206
+ const background = {
207
+ args: [VArg],
208
+ order: -1,
209
+ apply: (context, _pipe, background2) => {
210
+ background2 = String(background2);
211
+ if (!background2.startsWith("#") && (HEX_RE.test(background2) || SHORTHEX_RE.test(background2))) {
212
+ background2 = "#" + background2;
213
+ }
214
+ context.background = background2;
215
+ }
216
+ };
217
+ const enlarge = {
218
+ args: [],
219
+ apply: (context) => {
220
+ context.enlarge = true;
221
+ }
222
+ };
223
+ const width = {
224
+ args: [VArg],
225
+ apply: (context, pipe, width2) => {
226
+ return pipe.resize(width2, null, { withoutEnlargement: !context.enlarge });
227
+ }
228
+ };
229
+ const height = {
230
+ args: [VArg],
231
+ apply: (context, pipe, height2) => {
232
+ return pipe.resize(null, height2, { withoutEnlargement: !context.enlarge });
233
+ }
234
+ };
235
+ const resize = {
236
+ args: [VArg, VArg, VArg],
237
+ apply: (context, pipe, size) => {
238
+ let [width2, height2] = String(size).split("x").map((v) => Number(v));
239
+ if (!width2) {
240
+ return;
241
+ }
242
+ if (!height2) {
243
+ height2 = width2;
244
+ }
245
+ if (!context.enlarge) {
246
+ const clamped = clampDimensionsPreservingAspectRatio(context.meta, { width: width2, height: height2 });
247
+ width2 = clamped.width;
248
+ height2 = clamped.height;
249
+ }
250
+ return pipe.resize(width2, height2, {
251
+ fit: context.fit,
252
+ position: context.position,
253
+ background: context.background
254
+ });
255
+ }
256
+ };
257
+ const trim = {
258
+ args: [VArg],
259
+ apply: (_context, pipe, threshold2) => {
260
+ return pipe.trim(threshold2);
261
+ }
262
+ };
263
+ const extend = {
264
+ args: [VArg, VArg, VArg, VArg],
265
+ apply: (context, pipe, top, right, bottom, left) => {
266
+ return pipe.extend({
267
+ top,
268
+ left,
269
+ bottom,
270
+ right,
271
+ background: context.background
272
+ });
273
+ }
274
+ };
275
+ const extract = {
276
+ args: [VArg, VArg, VArg, VArg],
277
+ apply: (context, pipe, top, right, bottom, left) => {
278
+ return pipe.extend({
279
+ top,
280
+ left,
281
+ bottom,
282
+ right,
283
+ background: context.background
284
+ });
285
+ }
286
+ };
287
+ const rotate = {
288
+ args: [VArg],
289
+ apply: (context, pipe, angel) => {
290
+ return pipe.rotate(angel, {
291
+ background: context.background
292
+ });
293
+ }
294
+ };
295
+ const flip = {
296
+ args: [],
297
+ apply: (_context, pipe) => {
298
+ return pipe.flip();
299
+ }
300
+ };
301
+ const flop = {
302
+ args: [],
303
+ apply: (_context, pipe) => {
304
+ return pipe.flop();
305
+ }
306
+ };
307
+ const sharpen = {
308
+ args: [VArg, VArg, VArg],
309
+ apply: (_context, pipe, sigma, flat, jagged) => {
310
+ return pipe.sharpen(sigma, flat, jagged);
311
+ }
312
+ };
313
+ const median = {
314
+ args: [VArg, VArg, VArg],
315
+ apply: (_context, pipe, size) => {
316
+ return pipe.median(size);
317
+ }
318
+ };
319
+ const blur = {
320
+ args: [VArg, VArg, VArg],
321
+ apply: (_context, pipe) => {
322
+ return pipe.blur();
323
+ }
324
+ };
325
+ const flatten = {
326
+ args: [VArg, VArg, VArg],
327
+ apply: (context, pipe) => {
328
+ return pipe.flatten({
329
+ background: context.background
330
+ });
331
+ }
332
+ };
333
+ const gamma = {
334
+ args: [VArg, VArg, VArg],
335
+ apply: (_context, pipe, gamma2, gammaOut) => {
336
+ return pipe.gamma(gamma2, gammaOut);
337
+ }
338
+ };
339
+ const negate = {
340
+ args: [VArg, VArg, VArg],
341
+ apply: (_context, pipe) => {
342
+ return pipe.negate();
343
+ }
344
+ };
345
+ const normalize = {
346
+ args: [VArg, VArg, VArg],
347
+ apply: (_context, pipe) => {
348
+ return pipe.normalize();
349
+ }
350
+ };
351
+ const threshold = {
352
+ args: [VArg],
353
+ apply: (_context, pipe, threshold2) => {
354
+ return pipe.threshold(threshold2);
355
+ }
356
+ };
357
+ const modulate = {
358
+ args: [VArg],
359
+ apply: (_context, pipe, brightness, saturation, hue) => {
360
+ return pipe.modulate({
361
+ brightness,
362
+ saturation,
363
+ hue
364
+ });
365
+ }
366
+ };
367
+ const tint = {
368
+ args: [VArg],
369
+ apply: (_context, pipe, rgb) => {
370
+ return pipe.tint(rgb);
371
+ }
372
+ };
373
+ const grayscale = {
374
+ args: [VArg],
375
+ apply: (_context, pipe) => {
376
+ return pipe.grayscale();
377
+ }
378
+ };
379
+ const crop = extract;
380
+ const q = quality;
381
+ const b = background;
382
+ const w = width;
383
+ const h = height;
384
+ const s = resize;
385
+ const pos = position;
386
+
387
+ const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
388
+ function createIPX(userOptions) {
389
+ const defaults = {
390
+ dir: getEnv("IPX_DIR", "."),
391
+ domains: getEnv("IPX_DOMAINS", []),
392
+ alias: getEnv("IPX_ALIAS", {}),
393
+ sharp: {}
394
+ };
395
+ const options = defu__default(userOptions, defaults);
396
+ options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]]));
397
+ const ctx = {
398
+ sources: {}
399
+ };
400
+ if (options.dir) {
401
+ ctx.sources.filesystem = createFilesystemSource({
402
+ dir: options.dir
403
+ });
404
+ }
405
+ if (options.domains) {
406
+ ctx.sources.http = createHTTPSource({
407
+ domains: options.domains
408
+ });
409
+ }
410
+ return function ipx(id, modifiers = {}, reqOptions = {}) {
411
+ if (!id) {
412
+ throw createError("resource id is missing", 400);
413
+ }
414
+ id = ufo.hasProtocol(id) ? id : ufo.withLeadingSlash(id);
415
+ for (const base in options.alias) {
416
+ if (id.startsWith(base)) {
417
+ id = ufo.joinURL(options.alias[base], id.substr(base.length));
418
+ }
419
+ }
420
+ const getSrc = cachedPromise(() => {
421
+ const source = ufo.hasProtocol(id) ? "http" : "filesystem";
422
+ if (!ctx.sources[source]) {
423
+ throw createError("Unknown source: " + source, 400);
424
+ }
425
+ return ctx.sources[source](id, reqOptions);
426
+ });
427
+ const getData = cachedPromise(async () => {
428
+ const src = await getSrc();
429
+ const data = await src.getData();
430
+ const meta = imageMeta.imageMeta(data);
431
+ const mFormat = modifiers.f || modifiers.format;
432
+ let format = mFormat || meta.type;
433
+ if (format === "jpg") {
434
+ format = "jpeg";
435
+ }
436
+ if (meta.type === "svg" && !mFormat) {
437
+ return {
438
+ data,
439
+ format: "svg+xml",
440
+ meta
441
+ };
442
+ }
443
+ const animated = modifiers.animated !== void 0 || modifiers.a !== void 0;
444
+ if (animated) {
445
+ format = "webp";
446
+ }
447
+ const Sharp = await import('sharp').then((r) => r.default || r);
448
+ let sharp = Sharp(data, { animated });
449
+ Object.assign(sharp.options, options.sharp);
450
+ const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
451
+ const aKey = (a.handler.order || a.name || "").toString();
452
+ const bKey = (b.handler.order || b.name || "").toString();
453
+ return aKey.localeCompare(bKey);
454
+ });
455
+ const handlerCtx = { meta };
456
+ for (const h of handlers) {
457
+ sharp = applyHandler(handlerCtx, sharp, h.handler, h.args) || sharp;
458
+ }
459
+ if (SUPPORTED_FORMATS.includes(format)) {
460
+ sharp = sharp.toFormat(format, {
461
+ quality: handlerCtx.quality,
462
+ progressive: format === "jpeg"
463
+ });
464
+ }
465
+ const newData = await sharp.toBuffer();
466
+ return {
467
+ data: newData,
468
+ format,
469
+ meta
470
+ };
471
+ });
472
+ return {
473
+ src: getSrc,
474
+ data: getData
475
+ };
476
+ };
477
+ }
478
+
479
+ async function _handleRequest(req, ipx) {
480
+ const res = {
481
+ statusCode: 200,
482
+ statusMessage: "",
483
+ headers: {},
484
+ body: ""
485
+ };
486
+ const [modifiersStr = "", ...idSegments] = req.url.substr(1).split("/");
487
+ const id = ufo.decode(idSegments.join("/"));
488
+ if (!modifiersStr) {
489
+ throw createError("Modifiers is missing in path: " + req.url, 400);
490
+ }
491
+ if (!id || id === "/") {
492
+ throw createError("Resource id is missing: " + req.url, 400);
493
+ }
494
+ const modifiers = /* @__PURE__ */ Object.create(null);
495
+ if (modifiersStr !== "_") {
496
+ for (const p of modifiersStr.split(",")) {
497
+ const [key, value = ""] = p.split("_");
498
+ modifiers[key] = ufo.decode(value);
499
+ }
500
+ }
501
+ const img = ipx(id, modifiers, req.options);
502
+ const src = await img.src();
503
+ if (src.mtime) {
504
+ if (req.headers["if-modified-since"]) {
505
+ if (new Date(req.headers["if-modified-since"]) >= src.mtime) {
506
+ res.statusCode = 304;
507
+ return res;
508
+ }
509
+ }
510
+ res.headers["Last-Modified"] = +src.mtime + "";
511
+ }
512
+ if (src.maxAge !== void 0) {
513
+ res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
514
+ }
515
+ const { data, format } = await img.data();
516
+ const etag = getEtag__default(data);
517
+ res.headers.ETag = etag;
518
+ if (etag && req.headers["if-none-match"] === etag) {
519
+ res.statusCode = 304;
520
+ return res;
521
+ }
522
+ if (format) {
523
+ res.headers["Content-Type"] = `image/${format}`;
524
+ }
525
+ res.body = data;
526
+ return res;
527
+ }
528
+ function handleRequest(req, ipx) {
529
+ return _handleRequest(req, ipx).catch((err) => {
530
+ const statusCode = parseInt(err.statusCode) || 500;
531
+ const statusMessage = err.statusMessage ? xss__default(err.statusMessage) : `IPX Error (${statusCode})`;
532
+ if (process.env.NODE_ENV !== "production" && statusCode === 500) {
533
+ console.error(err);
534
+ }
535
+ return {
536
+ statusCode,
537
+ statusMessage,
538
+ body: statusMessage,
539
+ headers: {}
540
+ };
541
+ });
542
+ }
543
+ function createIPXMiddleware(ipx) {
544
+ return function IPXMiddleware(req, res) {
545
+ handleRequest({ url: req.url, headers: req.headers }, ipx).then((_res) => {
546
+ res.statusCode = _res.statusCode;
547
+ res.statusMessage = _res.statusMessage;
548
+ for (const name in _res.headers) {
549
+ res.setHeader(name, _res.headers[name]);
550
+ }
551
+ res.end(_res.body);
552
+ });
553
+ };
554
+ }
555
+
556
+ exports.createIPX = createIPX;
557
+ exports.createIPXMiddleware = createIPXMiddleware;
558
+ exports.handleRequest = handleRequest;