ipx 0.7.1 → 0.9.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/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
- <h2 align="center">Usage</h2>
9
+ ## Usage
10
10
 
11
11
  ### Quick Start
12
12
 
@@ -52,25 +52,27 @@ 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 | \_ | `http://localhost:3000/width_200/buffalo.png` |
58
- | height | \_ | `http://localhost:3000/height_200/buffalo.png` |
59
- | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
60
- | 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` |
61
- | quality | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
62
- | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
63
- | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
64
- | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
65
- | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
66
- | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
67
- | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
68
- | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
69
- | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
70
- | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
71
- | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
72
- | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
73
- | animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
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
- <h2 align="center">License</h2>
88
+ ## License
87
89
 
88
90
  MIT
package/bin/ipx.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs'
@@ -1,42 +1,34 @@
1
- #!/usr/bin/env node
2
-
3
1
  'use strict';
4
2
 
5
- const consola = require('consola');
6
- const listhen = require('listhen');
7
- const Sharp = require('sharp');
8
3
  const defu = require('defu');
9
4
  const imageMeta = require('image-meta');
10
5
  const ufo = require('ufo');
11
- const path = require('path');
6
+ const fs = require('fs');
7
+ const pathe = require('pathe');
12
8
  const isValidPath = require('is-valid-path');
13
- const fsExtra = require('fs-extra');
14
- const destr = require('destr');
15
9
  const http = require('http');
16
10
  const https = require('https');
17
- const fetch = require('node-fetch');
11
+ const ohmyfetch = require('ohmyfetch');
12
+ const destr = require('destr');
18
13
  const getEtag = require('etag');
19
14
  const xss = require('xss');
20
15
 
21
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
16
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
22
17
 
23
- const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
24
- const Sharp__default = /*#__PURE__*/_interopDefaultLegacy(Sharp);
25
18
  const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
26
- const imageMeta__default = /*#__PURE__*/_interopDefaultLegacy(imageMeta);
27
19
  const isValidPath__default = /*#__PURE__*/_interopDefaultLegacy(isValidPath);
28
- const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
29
20
  const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
30
21
  const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
31
- const fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
22
+ const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
32
23
  const getEtag__default = /*#__PURE__*/_interopDefaultLegacy(getEtag);
33
24
  const xss__default = /*#__PURE__*/_interopDefaultLegacy(xss);
34
25
 
35
- const Handlers = /*#__PURE__*/Object.freeze({
26
+ const Handlers = {
36
27
  __proto__: null,
37
28
  get quality () { return quality; },
38
29
  get fit () { return fit; },
39
30
  get background () { return background; },
31
+ get enlarge () { return enlarge; },
40
32
  get width () { return width; },
41
33
  get height () { return height; },
42
34
  get resize () { return resize; },
@@ -63,11 +55,10 @@ const Handlers = /*#__PURE__*/Object.freeze({
63
55
  get w () { return w; },
64
56
  get h () { return h; },
65
57
  get s () { return s; }
66
- });
58
+ };
67
59
 
68
60
  function getEnv(name, defaultValue) {
69
- var _a;
70
- return (_a = destr__default['default'](process.env[name])) != null ? _a : defaultValue;
61
+ return destr__default(process.env[name]) ?? defaultValue;
71
62
  }
72
63
  function cachedPromise(fn) {
73
64
  let p;
@@ -89,15 +80,15 @@ function createError(message, statusCode) {
89
80
  }
90
81
 
91
82
  const createFilesystemSource = (options) => {
92
- const rootDir = path.resolve(options.dir);
83
+ const rootDir = pathe.resolve(options.dir);
93
84
  return async (id) => {
94
- const fsPath = path.resolve(path.join(rootDir, id));
95
- if (!isValidPath__default['default'](id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
85
+ const fsPath = pathe.resolve(pathe.join(rootDir, id));
86
+ if (!isValidPath__default(id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
96
87
  throw createError("Forbidden path:" + id, 403);
97
88
  }
98
89
  let stats;
99
90
  try {
100
- stats = await fsExtra.stat(fsPath);
91
+ stats = await fs.promises.stat(fsPath);
101
92
  } catch (err) {
102
93
  if (err.code === "ENOENT") {
103
94
  throw createError("File not found: " + fsPath, 404);
@@ -111,14 +102,14 @@ const createFilesystemSource = (options) => {
111
102
  return {
112
103
  mtime: stats.mtime,
113
104
  maxAge: options.maxAge || 300,
114
- getData: cachedPromise(() => fsExtra.readFile(fsPath))
105
+ getData: cachedPromise(() => fs.promises.readFile(fsPath))
115
106
  };
116
107
  };
117
108
  };
118
109
 
119
110
  const createHTTPSource = (options) => {
120
- const httpsAgent = new https__default['default'].Agent({ keepAlive: true });
121
- const httpAgent = new http__default['default'].Agent({ keepAlive: true });
111
+ const httpsAgent = new https__default.Agent({ keepAlive: true });
112
+ const httpAgent = new http__default.Agent({ keepAlive: true });
122
113
  let domains = options.domains || [];
123
114
  if (typeof domains === "string") {
124
115
  domains = domains.split(",").map((s) => s.trim());
@@ -129,10 +120,10 @@ const createHTTPSource = (options) => {
129
120
  if (!parsedUrl.host) {
130
121
  throw createError("Hostname is missing: " + id, 403);
131
122
  }
132
- if (!(reqOptions == null ? void 0 : reqOptions.bypassDomain) && !hosts.find((host) => parsedUrl.host === host)) {
123
+ if (!reqOptions?.bypassDomain && !hosts.find((host) => parsedUrl.host === host)) {
133
124
  throw createError("Forbidden host: " + parsedUrl.host, 403);
134
125
  }
135
- const response = await fetch__default['default'](id, {
126
+ const response = await ohmyfetch.fetch(id, {
136
127
  agent: id.startsWith("https") ? httpsAgent : httpAgent
137
128
  });
138
129
  if (!response.ok) {
@@ -160,7 +151,7 @@ const createHTTPSource = (options) => {
160
151
  };
161
152
 
162
153
  function VArg(arg) {
163
- return destr__default['default'](arg);
154
+ return destr__default(arg);
164
155
  }
165
156
  function parseArgs(args, mappers) {
166
157
  const vargs = args.split("_");
@@ -173,6 +164,19 @@ function applyHandler(ctx, pipe, handler, argsStr) {
173
164
  const args = handler.args ? parseArgs(argsStr, handler.args) : [];
174
165
  return handler.apply(ctx, pipe, ...args);
175
166
  }
167
+ function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
168
+ const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
169
+ let { width, height } = desiredDimensions;
170
+ if (width > sourceDimensions.width) {
171
+ width = sourceDimensions.width;
172
+ height = Math.round(sourceDimensions.width / desiredAspectRatio);
173
+ }
174
+ if (height > sourceDimensions.height) {
175
+ height = sourceDimensions.height;
176
+ width = Math.round(sourceDimensions.height * desiredAspectRatio);
177
+ }
178
+ return { width, height };
179
+ }
176
180
 
177
181
  const quality = {
178
182
  args: [VArg],
@@ -201,22 +205,33 @@ const background = {
201
205
  context.background = background2;
202
206
  }
203
207
  };
208
+ const enlarge = {
209
+ args: [],
210
+ apply: (context) => {
211
+ context.enlarge = true;
212
+ }
213
+ };
204
214
  const width = {
205
215
  args: [VArg],
206
- apply: (_context, pipe, width2) => {
207
- return pipe.resize(width2, null);
216
+ apply: (context, pipe, width2) => {
217
+ return pipe.resize(width2, null, { withoutEnlargement: !context.enlarge });
208
218
  }
209
219
  };
210
220
  const height = {
211
221
  args: [VArg],
212
- apply: (_context, pipe, height2) => {
213
- return pipe.resize(null, height2);
222
+ apply: (context, pipe, height2) => {
223
+ return pipe.resize(null, height2, { withoutEnlargement: !context.enlarge });
214
224
  }
215
225
  };
216
226
  const resize = {
217
227
  args: [VArg, VArg, VArg],
218
228
  apply: (context, pipe, size) => {
219
- const [width2, height2] = String(size).split("x").map((v) => Number(v));
229
+ let [width2, height2] = String(size).split("x").map((v) => Number(v));
230
+ if (!context.enlarge) {
231
+ const clamped = clampDimensionsPreservingAspectRatio(context.meta, { width: width2, height: height2 });
232
+ width2 = clamped.width;
233
+ height2 = clamped.height;
234
+ }
220
235
  return pipe.resize(width2, height2, {
221
236
  fit: context.fit,
222
237
  background: context.background
@@ -360,7 +375,7 @@ function createIPX(userOptions) {
360
375
  alias: getEnv("IPX_ALIAS", {}),
361
376
  sharp: {}
362
377
  };
363
- const options = defu__default['default'](userOptions, defaults);
378
+ const options = defu__default(userOptions, defaults);
364
379
  options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]]));
365
380
  const ctx = {
366
381
  sources: {}
@@ -395,7 +410,7 @@ function createIPX(userOptions) {
395
410
  const getData = cachedPromise(async () => {
396
411
  const src = await getSrc();
397
412
  const data = await src.getData();
398
- const meta = imageMeta__default['default'](data);
413
+ const meta = imageMeta.imageMeta(data);
399
414
  const mFormat = modifiers.f || modifiers.format;
400
415
  let format = mFormat || meta.type;
401
416
  if (format === "jpg") {
@@ -412,14 +427,15 @@ function createIPX(userOptions) {
412
427
  if (animated) {
413
428
  format = "webp";
414
429
  }
415
- let sharp = Sharp__default['default'](data, { animated });
430
+ const Sharp = await import('sharp').then((r) => r.default || r);
431
+ let sharp = Sharp(data, { animated });
416
432
  Object.assign(sharp.options, options.sharp);
417
433
  const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
418
434
  const aKey = (a.handler.order || a.name || "").toString();
419
435
  const bKey = (b.handler.order || b.name || "").toString();
420
436
  return aKey.localeCompare(bKey);
421
437
  });
422
- const handlerCtx = {};
438
+ const handlerCtx = { meta };
423
439
  for (const h of handlers) {
424
440
  sharp = applyHandler(handlerCtx, sharp, h.handler, h.args) || sharp;
425
441
  }
@@ -480,7 +496,7 @@ async function _handleRequest(req, ipx) {
480
496
  res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
481
497
  }
482
498
  const { data, format } = await img.data();
483
- const etag = getEtag__default['default'](data);
499
+ const etag = getEtag__default(data);
484
500
  res.headers.ETag = etag;
485
501
  if (etag && req.headers["if-none-match"] === etag) {
486
502
  res.statusCode = 304;
@@ -495,7 +511,7 @@ async function _handleRequest(req, ipx) {
495
511
  function handleRequest(req, ipx) {
496
512
  return _handleRequest(req, ipx).catch((err) => {
497
513
  const statusCode = parseInt(err.statusCode) || 500;
498
- const statusMessage = err.statusMessage ? xss__default['default'](err.statusMessage) : `IPX Error (${statusCode})`;
514
+ const statusMessage = err.statusMessage ? xss__default(err.statusMessage) : `IPX Error (${statusCode})`;
499
515
  if (process.env.NODE_ENV !== "production" && statusCode === 500) {
500
516
  console.error(err);
501
517
  }
@@ -520,14 +536,6 @@ function createIPXMiddleware(ipx) {
520
536
  };
521
537
  }
522
538
 
523
- async function main() {
524
- const ipx = createIPX({});
525
- const middleware = createIPXMiddleware(ipx);
526
- await listhen.listen(middleware, {
527
- clipboard: false
528
- });
529
- }
530
- main().catch((err) => {
531
- consola__default['default'].error(err);
532
- process.exit(1);
533
- });
539
+ exports.createIPX = createIPX;
540
+ exports.createIPXMiddleware = createIPXMiddleware;
541
+ exports.handleRequest = handleRequest;