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