ipx 4.0.0-alpha.1 → 4.0.0-beta.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 +495 -78
- package/dist/THIRD-PARTY-LICENSES.md +593 -0
- package/dist/_chunks/libs/@fastify/accept-negotiator.mjs +71 -1
- package/dist/_chunks/libs/boolbase.mjs +12 -1
- package/dist/_chunks/libs/crossws.d.mts +1 -0
- package/dist/_chunks/libs/css-select.mjs +4136 -3
- package/dist/_chunks/libs/css-tree.mjs +31045 -51
- package/dist/_chunks/libs/csso.mjs +1821 -3
- package/dist/_chunks/libs/etag.mjs +25 -1
- package/dist/_chunks/libs/h3.d.mts +3 -0
- package/dist/_chunks/libs/h3.mjs +309 -2
- package/dist/_chunks/libs/image-meta.d.mts +8 -0
- package/dist/_chunks/libs/image-meta.mjs +557 -2
- package/dist/_chunks/libs/sax.mjs +837 -10
- package/dist/_chunks/libs/svgo.mjs +5883 -11
- package/dist/_chunks/libs/ufo.mjs +34 -1
- package/dist/_chunks/node-fs.mjs +1031 -245
- package/dist/_chunks/rolldown-runtime.mjs +24 -28
- package/dist/_chunks/svgo-node.mjs +2 -10
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +3 -13
- package/dist/index.d.mts +185 -30
- package/dist/index.mjs +3 -19
- package/package.json +24 -21
package/dist/_chunks/node-fs.mjs
CHANGED
|
@@ -1,19 +1,90 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { join, parse, resolve } from "pathe";
|
|
8
|
-
|
|
9
|
-
//#region src/handlers/utils.ts
|
|
1
|
+
import { __exportAll, __toESM } from "./rolldown-runtime.mjs";
|
|
2
|
+
import { hasProtocol, joinURL, withLeadingSlash } from "./libs/ufo.mjs";
|
|
3
|
+
import { HTTPError, defineEventHandler } from "./libs/h3.mjs";
|
|
4
|
+
import { imageMeta } from "./libs/image-meta.mjs";
|
|
5
|
+
import { require_etag } from "./libs/etag.mjs";
|
|
6
|
+
import { require_accept_negotiator } from "./libs/@fastify/accept-negotiator.mjs";
|
|
10
7
|
function VArg(argument) {
|
|
11
8
|
if (argument === "Infinity") return Infinity;
|
|
9
|
+
if (argument === "undefined") return;
|
|
12
10
|
try {
|
|
13
11
|
const val = JSON.parse(argument);
|
|
14
12
|
const t = typeof val;
|
|
15
13
|
if (t === "boolean" || t === "number" || t === "string" || val === null) return val;
|
|
16
14
|
} catch {}
|
|
15
|
+
return argument;
|
|
16
|
+
}
|
|
17
|
+
function invalidArg(name, argument, expected) {
|
|
18
|
+
throw new HTTPError({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
statusText: "IPX_INVALID_MODIFIER_ARG",
|
|
21
|
+
message: `Invalid \`${name}\` modifier argument: \`${argument}\` (expected ${expected})`
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function isOmitted(argument) {
|
|
25
|
+
return argument === void 0 || argument === "" || argument === "undefined";
|
|
26
|
+
}
|
|
27
|
+
function VNumber(name, options = {}) {
|
|
28
|
+
const { min, max, integer } = options;
|
|
29
|
+
const expected = [integer ? "an integer" : "a number", min !== void 0 && max !== void 0 ? `between ${min} and ${max}` : min !== void 0 && `greater than or equal to ${min}` || max !== void 0 && `less than or equal to ${max}` || ""].filter(Boolean).join(" ");
|
|
30
|
+
return (argument) => {
|
|
31
|
+
if (isOmitted(argument)) return;
|
|
32
|
+
const value = VArg(argument);
|
|
33
|
+
if (typeof value === "number" && Number.isFinite(value) && (!integer || Number.isInteger(value)) && (min === void 0 || value >= min) && (max === void 0 || value <= max)) return value;
|
|
34
|
+
invalidArg(name, argument, expected);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function VEnum(name, values) {
|
|
38
|
+
const expected = `one of: ${values.join(", ")}`;
|
|
39
|
+
return (argument) => {
|
|
40
|
+
if (isOmitted(argument)) return;
|
|
41
|
+
if (values.includes(argument)) return argument;
|
|
42
|
+
invalidArg(name, argument, expected);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function VBoolean(name) {
|
|
46
|
+
return (argument) => {
|
|
47
|
+
if (isOmitted(argument)) return;
|
|
48
|
+
if (argument === "true" || argument === "1") return true;
|
|
49
|
+
if (argument === "false" || argument === "0") return false;
|
|
50
|
+
invalidArg(name, argument, "`true` or `false`");
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const HEX_COLOR_RE = /^#?(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
54
|
+
const FN_COLOR_RE = /^(?:rgba?|hsla?|hwb)\([\d\s%,./-]+\)$/i;
|
|
55
|
+
const NAMED_COLOR_RE = /^[a-z]{3,20}$/i;
|
|
56
|
+
function VColor(name) {
|
|
57
|
+
return (argument) => {
|
|
58
|
+
if (isOmitted(argument)) return;
|
|
59
|
+
if (HEX_COLOR_RE.test(argument)) return argument.startsWith("#") ? argument : `#${argument}`;
|
|
60
|
+
if (FN_COLOR_RE.test(argument) || NAMED_COLOR_RE.test(argument)) return argument;
|
|
61
|
+
invalidArg(name, argument, "a hex (`f00`, `ff0000`) or named (`red`) colour");
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const SIZE_RE = /^(\d+)(?:x(\d+))?$/;
|
|
65
|
+
function VSize(name) {
|
|
66
|
+
return (argument) => {
|
|
67
|
+
if (isOmitted(argument)) return;
|
|
68
|
+
const match = SIZE_RE.exec(argument);
|
|
69
|
+
const width = Number(match?.[1]);
|
|
70
|
+
const height = match?.[2] === void 0 ? width : Number(match[2]);
|
|
71
|
+
if (match && width > 0 && height > 0) return {
|
|
72
|
+
width,
|
|
73
|
+
height
|
|
74
|
+
};
|
|
75
|
+
invalidArg(name, argument, "`{width}x{height}` of positive integers");
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function VRequired(name, mapper) {
|
|
79
|
+
return (argument) => {
|
|
80
|
+
const value = mapper(argument);
|
|
81
|
+
if (value === void 0) throw new HTTPError({
|
|
82
|
+
statusCode: 400,
|
|
83
|
+
statusText: "IPX_MISSING_MODIFIER_ARG",
|
|
84
|
+
message: `Missing \`${name}\` modifier argument`
|
|
85
|
+
});
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
17
88
|
}
|
|
18
89
|
function parseArgs(arguments_, mappers) {
|
|
19
90
|
const vargs = arguments_.split("_");
|
|
@@ -24,33 +95,125 @@ function getHandler(key) {
|
|
|
24
95
|
}
|
|
25
96
|
function applyHandler(context, pipe, handler, argumentsString) {
|
|
26
97
|
const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
|
|
27
|
-
|
|
98
|
+
try {
|
|
99
|
+
return handler.apply(context, pipe, ...arguments_);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw asModifierError(error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function asModifierError(error) {
|
|
105
|
+
if (HTTPError.isError(error)) return error;
|
|
106
|
+
return new HTTPError({
|
|
107
|
+
statusCode: 400,
|
|
108
|
+
statusText: "IPX_INVALID_MODIFIER",
|
|
109
|
+
message: `Cannot apply modifier: ${error?.message || error}`,
|
|
110
|
+
cause: error
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
114
|
+
function opacityOverlay(opacity, background) {
|
|
115
|
+
return background ? {
|
|
116
|
+
input: Buffer.from(`<svg xmlns="${SVG_NS}" width="1" height="1"><rect width="1" height="1" fill="${background}" fill-opacity="${1 - opacity}"/></svg>`),
|
|
117
|
+
tile: true,
|
|
118
|
+
blend: "over"
|
|
119
|
+
} : {
|
|
120
|
+
input: { create: {
|
|
121
|
+
width: 1,
|
|
122
|
+
height: 1,
|
|
123
|
+
channels: 4,
|
|
124
|
+
background: {
|
|
125
|
+
r: 0,
|
|
126
|
+
g: 0,
|
|
127
|
+
b: 0,
|
|
128
|
+
alpha: opacity
|
|
129
|
+
}
|
|
130
|
+
} },
|
|
131
|
+
tile: true,
|
|
132
|
+
blend: "dest-in"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function clampToMaxDimension(max, desired, source) {
|
|
136
|
+
const { width, height } = desired;
|
|
137
|
+
if (!max || width === void 0 && height === void 0) return desired;
|
|
138
|
+
const ratio = source?.width && source?.height ? source.width / source.height : void 0;
|
|
139
|
+
const effectiveWidth = width ?? (ratio && height ? height * ratio : void 0);
|
|
140
|
+
const effectiveHeight = height ?? (ratio && width ? width / ratio : void 0);
|
|
141
|
+
const scale = Math.min(effectiveWidth ? max / effectiveWidth : 1, effectiveHeight ? max / effectiveHeight : 1);
|
|
142
|
+
if (scale >= 1) return desired;
|
|
143
|
+
const clamp = (value) => value === void 0 ? void 0 : Math.max(1, Math.min(max, Math.round(value * scale)));
|
|
144
|
+
return {
|
|
145
|
+
width: clamp(width),
|
|
146
|
+
height: clamp(height)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function resizeOutputDimensions(desired, source, options = {}) {
|
|
150
|
+
const { width, height } = desired;
|
|
151
|
+
if (width === void 0 && height === void 0) return;
|
|
152
|
+
const sourceWidth = source?.width;
|
|
153
|
+
const sourceHeight = source?.height;
|
|
154
|
+
if (!sourceWidth || !sourceHeight) return desired;
|
|
155
|
+
const derive = (value) => Math.max(1, Math.round(value));
|
|
156
|
+
let out = {
|
|
157
|
+
width: width ?? derive(height * sourceWidth / sourceHeight),
|
|
158
|
+
height: height ?? derive(width * sourceHeight / sourceWidth)
|
|
159
|
+
};
|
|
160
|
+
if (options.fit === "outside" && width !== void 0 && height !== void 0) {
|
|
161
|
+
const scale = Math.max(width / sourceWidth, height / sourceHeight);
|
|
162
|
+
out = {
|
|
163
|
+
width: derive(sourceWidth * scale),
|
|
164
|
+
height: derive(sourceHeight * scale)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return options.withoutEnlargement ? {
|
|
168
|
+
width: Math.min(out.width, sourceWidth),
|
|
169
|
+
height: Math.min(out.height, sourceHeight)
|
|
170
|
+
} : out;
|
|
171
|
+
}
|
|
172
|
+
function clampExtendEdges(max, edges, source) {
|
|
173
|
+
if (!max) return edges;
|
|
174
|
+
const [top, bottom] = clampEdges(max - (source?.height || 0), edges.top, edges.bottom);
|
|
175
|
+
const [left, right] = clampEdges(max - (source?.width || 0), edges.left, edges.right);
|
|
176
|
+
return {
|
|
177
|
+
top,
|
|
178
|
+
right,
|
|
179
|
+
bottom,
|
|
180
|
+
left
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function clampEdges(available, a, b) {
|
|
184
|
+
const total = (a || 0) + (b || 0);
|
|
185
|
+
if (total === 0 || total <= available) return [a, b];
|
|
186
|
+
const scale = Math.max(0, available) / total;
|
|
187
|
+
const clamp = (value) => value === void 0 ? void 0 : Math.floor(value * scale);
|
|
188
|
+
return [clamp(a), clamp(b)];
|
|
28
189
|
}
|
|
29
190
|
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
30
191
|
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
31
|
-
let { width
|
|
32
|
-
if (sourceDimensions.width && width
|
|
33
|
-
width
|
|
34
|
-
height
|
|
192
|
+
let { width, height } = desiredDimensions;
|
|
193
|
+
if (sourceDimensions.width && width > sourceDimensions.width) {
|
|
194
|
+
width = sourceDimensions.width;
|
|
195
|
+
height = Math.round(sourceDimensions.width / desiredAspectRatio);
|
|
35
196
|
}
|
|
36
|
-
if (sourceDimensions.height && height
|
|
37
|
-
height
|
|
38
|
-
width
|
|
197
|
+
if (sourceDimensions.height && height > sourceDimensions.height) {
|
|
198
|
+
height = sourceDimensions.height;
|
|
199
|
+
width = Math.round(sourceDimensions.height * desiredAspectRatio);
|
|
39
200
|
}
|
|
40
201
|
return {
|
|
41
|
-
width
|
|
42
|
-
height
|
|
202
|
+
width,
|
|
203
|
+
height
|
|
43
204
|
};
|
|
44
205
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//#region src/handlers/handlers.ts
|
|
48
|
-
var handlers_exports = /* @__PURE__ */ __export({
|
|
206
|
+
var handlers_exports = /* @__PURE__ */ __exportAll({
|
|
207
|
+
autoorient: () => autoorient,
|
|
49
208
|
b: () => b,
|
|
50
209
|
background: () => background,
|
|
51
210
|
blur: () => blur,
|
|
211
|
+
brightness: () => brightness,
|
|
212
|
+
clahe: () => clahe,
|
|
52
213
|
crop: () => crop,
|
|
214
|
+
dilate: () => dilate,
|
|
53
215
|
enlarge: () => enlarge,
|
|
216
|
+
erode: () => erode,
|
|
54
217
|
extend: () => extend,
|
|
55
218
|
extract: () => extract,
|
|
56
219
|
fit: () => fit,
|
|
@@ -61,11 +224,15 @@ var handlers_exports = /* @__PURE__ */ __export({
|
|
|
61
224
|
grayscale: () => grayscale,
|
|
62
225
|
h: () => h,
|
|
63
226
|
height: () => height,
|
|
227
|
+
hue: () => hue,
|
|
64
228
|
kernel: () => kernel,
|
|
229
|
+
lightness: () => lightness,
|
|
230
|
+
linear: () => linear,
|
|
65
231
|
median: () => median,
|
|
66
232
|
modulate: () => modulate,
|
|
67
233
|
negate: () => negate,
|
|
68
234
|
normalize: () => normalize,
|
|
235
|
+
opacity: () => opacity,
|
|
69
236
|
pos: () => pos,
|
|
70
237
|
position: () => position,
|
|
71
238
|
q: () => q,
|
|
@@ -73,43 +240,93 @@ var handlers_exports = /* @__PURE__ */ __export({
|
|
|
73
240
|
resize: () => resize,
|
|
74
241
|
rotate: () => rotate,
|
|
75
242
|
s: () => s,
|
|
243
|
+
saturation: () => saturation,
|
|
76
244
|
sharpen: () => sharpen,
|
|
77
245
|
threshold: () => threshold,
|
|
78
246
|
tint: () => tint,
|
|
79
247
|
trim: () => trim,
|
|
248
|
+
unflatten: () => unflatten,
|
|
80
249
|
w: () => w,
|
|
81
250
|
width: () => width
|
|
82
251
|
});
|
|
83
252
|
const quality = {
|
|
84
|
-
args: [
|
|
253
|
+
args: [VNumber("quality", {
|
|
254
|
+
min: 1,
|
|
255
|
+
max: 100,
|
|
256
|
+
integer: true
|
|
257
|
+
})],
|
|
85
258
|
order: -1,
|
|
86
|
-
apply: (context, _pipe, quality
|
|
87
|
-
context.quality = quality
|
|
259
|
+
apply: (context, _pipe, quality) => {
|
|
260
|
+
context.quality = quality;
|
|
88
261
|
}
|
|
89
262
|
};
|
|
90
263
|
const fit = {
|
|
91
|
-
args: [
|
|
264
|
+
args: [VEnum("fit", [
|
|
265
|
+
"contain",
|
|
266
|
+
"cover",
|
|
267
|
+
"fill",
|
|
268
|
+
"inside",
|
|
269
|
+
"outside"
|
|
270
|
+
])],
|
|
92
271
|
order: -1,
|
|
93
|
-
apply: (context, _pipe, fit
|
|
94
|
-
context.fit = fit
|
|
272
|
+
apply: (context, _pipe, fit) => {
|
|
273
|
+
context.fit = fit;
|
|
95
274
|
}
|
|
96
275
|
};
|
|
276
|
+
const POSITIONS = [
|
|
277
|
+
"top",
|
|
278
|
+
"right top",
|
|
279
|
+
"right",
|
|
280
|
+
"right bottom",
|
|
281
|
+
"bottom",
|
|
282
|
+
"left bottom",
|
|
283
|
+
"left",
|
|
284
|
+
"left top",
|
|
285
|
+
"north",
|
|
286
|
+
"northeast",
|
|
287
|
+
"east",
|
|
288
|
+
"southeast",
|
|
289
|
+
"south",
|
|
290
|
+
"southwest",
|
|
291
|
+
"west",
|
|
292
|
+
"northwest",
|
|
293
|
+
"center",
|
|
294
|
+
"centre",
|
|
295
|
+
"entropy",
|
|
296
|
+
"attention"
|
|
297
|
+
];
|
|
298
|
+
const GRAVITIES = /* @__PURE__ */ new Set([
|
|
299
|
+
0,
|
|
300
|
+
1,
|
|
301
|
+
2,
|
|
302
|
+
3,
|
|
303
|
+
4,
|
|
304
|
+
5,
|
|
305
|
+
6,
|
|
306
|
+
7,
|
|
307
|
+
8,
|
|
308
|
+
16,
|
|
309
|
+
17
|
|
310
|
+
]);
|
|
311
|
+
const VPosition = (name) => {
|
|
312
|
+
const vEnum = VEnum(name, POSITIONS);
|
|
313
|
+
return (argument) => {
|
|
314
|
+
if (/^\d+$/.test(argument) && GRAVITIES.has(Number(argument))) return Number(argument);
|
|
315
|
+
return vEnum(argument?.replace("-", " "));
|
|
316
|
+
};
|
|
317
|
+
};
|
|
97
318
|
const position = {
|
|
98
|
-
args: [
|
|
319
|
+
args: [VPosition("position")],
|
|
99
320
|
order: -1,
|
|
100
|
-
apply: (context, _pipe, position
|
|
101
|
-
context.position = position
|
|
321
|
+
apply: (context, _pipe, position) => {
|
|
322
|
+
context.position = position;
|
|
102
323
|
}
|
|
103
324
|
};
|
|
104
|
-
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
|
|
105
|
-
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
|
|
106
325
|
const background = {
|
|
107
|
-
args: [
|
|
326
|
+
args: [VColor("background")],
|
|
108
327
|
order: -1,
|
|
109
|
-
apply: (context, _pipe, background
|
|
110
|
-
background
|
|
111
|
-
if (!background$1.startsWith("#") && (HEX_RE.test(background$1) || SHORTHEX_RE.test(background$1))) background$1 = "#" + background$1;
|
|
112
|
-
context.background = background$1;
|
|
328
|
+
apply: (context, _pipe, background) => {
|
|
329
|
+
context.background = background;
|
|
113
330
|
}
|
|
114
331
|
};
|
|
115
332
|
const enlarge = {
|
|
@@ -119,42 +336,63 @@ const enlarge = {
|
|
|
119
336
|
}
|
|
120
337
|
};
|
|
121
338
|
const kernel = {
|
|
122
|
-
args: [
|
|
123
|
-
|
|
124
|
-
|
|
339
|
+
args: [VEnum("kernel", [
|
|
340
|
+
"nearest",
|
|
341
|
+
"linear",
|
|
342
|
+
"cubic",
|
|
343
|
+
"mitchell",
|
|
344
|
+
"lanczos2",
|
|
345
|
+
"lanczos3",
|
|
346
|
+
"mks2013",
|
|
347
|
+
"mks2021"
|
|
348
|
+
])],
|
|
349
|
+
apply: (context, _pipe, kernel) => {
|
|
350
|
+
context.kernel = kernel;
|
|
125
351
|
}
|
|
126
352
|
};
|
|
127
353
|
const width = {
|
|
128
|
-
args: [
|
|
129
|
-
|
|
130
|
-
|
|
354
|
+
args: [VNumber("width", {
|
|
355
|
+
min: 1,
|
|
356
|
+
integer: true
|
|
357
|
+
})],
|
|
358
|
+
apply: (context, pipe, width) => {
|
|
359
|
+
const clamped = clampToMaxDimension(context.maxOutputDimension, { width }, context.meta);
|
|
360
|
+
const withoutEnlargement = !context.enlarge;
|
|
361
|
+
context.outputDimensions = resizeOutputDimensions(clamped, context.meta, { withoutEnlargement });
|
|
362
|
+
return pipe.resize(clamped.width, void 0, { withoutEnlargement });
|
|
131
363
|
}
|
|
132
364
|
};
|
|
133
365
|
const height = {
|
|
134
|
-
args: [
|
|
135
|
-
|
|
136
|
-
|
|
366
|
+
args: [VNumber("height", {
|
|
367
|
+
min: 1,
|
|
368
|
+
integer: true
|
|
369
|
+
})],
|
|
370
|
+
apply: (context, pipe, height) => {
|
|
371
|
+
const clamped = clampToMaxDimension(context.maxOutputDimension, { height }, context.meta);
|
|
372
|
+
const withoutEnlargement = !context.enlarge;
|
|
373
|
+
context.outputDimensions = resizeOutputDimensions(clamped, context.meta, { withoutEnlargement });
|
|
374
|
+
return pipe.resize(void 0, clamped.height, { withoutEnlargement });
|
|
137
375
|
}
|
|
138
376
|
};
|
|
139
377
|
const resize = {
|
|
140
|
-
args: [
|
|
141
|
-
VArg,
|
|
142
|
-
VArg,
|
|
143
|
-
VArg
|
|
144
|
-
],
|
|
378
|
+
args: [VSize("resize")],
|
|
145
379
|
apply: (context, pipe, size) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!height$1) height$1 = width$1;
|
|
380
|
+
if (!size) return;
|
|
381
|
+
let { width, height } = size;
|
|
149
382
|
if (!context.enlarge) {
|
|
150
383
|
const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
|
|
151
|
-
width
|
|
152
|
-
height
|
|
384
|
+
width,
|
|
385
|
+
height
|
|
153
386
|
});
|
|
154
|
-
width
|
|
155
|
-
height
|
|
387
|
+
width = clamped.width;
|
|
388
|
+
height = clamped.height;
|
|
156
389
|
}
|
|
157
|
-
|
|
390
|
+
const capped = clampToMaxDimension(context.maxOutputDimension, {
|
|
391
|
+
width,
|
|
392
|
+
height
|
|
393
|
+
});
|
|
394
|
+
context.outputDimensions = resizeOutputDimensions(capped, context.meta, { fit: context.fit });
|
|
395
|
+
return pipe.resize(capped.width, capped.height, {
|
|
158
396
|
fit: context.fit,
|
|
159
397
|
position: context.position,
|
|
160
398
|
background: context.background,
|
|
@@ -163,48 +401,94 @@ const resize = {
|
|
|
163
401
|
}
|
|
164
402
|
};
|
|
165
403
|
const trim = {
|
|
166
|
-
args: [
|
|
167
|
-
apply: (_context, pipe, threshold
|
|
168
|
-
return pipe.trim(threshold
|
|
404
|
+
args: [VNumber("trim", { min: 0 })],
|
|
405
|
+
apply: (_context, pipe, threshold) => {
|
|
406
|
+
return pipe.trim({ threshold });
|
|
169
407
|
}
|
|
170
408
|
};
|
|
409
|
+
const EXTEND_WITH = [
|
|
410
|
+
"background",
|
|
411
|
+
"copy",
|
|
412
|
+
"repeat",
|
|
413
|
+
"mirror"
|
|
414
|
+
];
|
|
415
|
+
const VExtendEdge = (name) => VNumber(name, {
|
|
416
|
+
min: 0,
|
|
417
|
+
max: 1e4,
|
|
418
|
+
integer: true
|
|
419
|
+
});
|
|
171
420
|
const extend = {
|
|
172
421
|
args: [
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
422
|
+
VExtendEdge("extend.top"),
|
|
423
|
+
VExtendEdge("extend.right"),
|
|
424
|
+
VExtendEdge("extend.bottom"),
|
|
425
|
+
VExtendEdge("extend.left"),
|
|
426
|
+
VEnum("extend.extendWith", EXTEND_WITH)
|
|
177
427
|
],
|
|
178
|
-
|
|
179
|
-
|
|
428
|
+
order: 1,
|
|
429
|
+
apply: (context, pipe, top, right, bottom, left, extendWith) => {
|
|
430
|
+
const edges = clampExtendEdges(context.maxOutputDimension, {
|
|
180
431
|
top,
|
|
181
|
-
left,
|
|
182
|
-
bottom,
|
|
183
432
|
right,
|
|
184
|
-
|
|
433
|
+
bottom,
|
|
434
|
+
left
|
|
435
|
+
}, context.outputDimensions ?? context.meta);
|
|
436
|
+
return pipe.extend({
|
|
437
|
+
top: edges.top,
|
|
438
|
+
left: edges.left,
|
|
439
|
+
bottom: edges.bottom,
|
|
440
|
+
right: edges.right,
|
|
441
|
+
background: context.background,
|
|
442
|
+
extendWith
|
|
185
443
|
});
|
|
186
444
|
}
|
|
187
445
|
};
|
|
446
|
+
const MAX_EXTRACT = 1e8;
|
|
188
447
|
const extract = {
|
|
189
448
|
args: [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
449
|
+
VRequired("extract.left", VNumber("extract.left", {
|
|
450
|
+
min: 0,
|
|
451
|
+
max: MAX_EXTRACT,
|
|
452
|
+
integer: true
|
|
453
|
+
})),
|
|
454
|
+
VRequired("extract.top", VNumber("extract.top", {
|
|
455
|
+
min: 0,
|
|
456
|
+
max: MAX_EXTRACT,
|
|
457
|
+
integer: true
|
|
458
|
+
})),
|
|
459
|
+
VRequired("extract.width", VNumber("extract.width", {
|
|
460
|
+
min: 1,
|
|
461
|
+
max: MAX_EXTRACT,
|
|
462
|
+
integer: true
|
|
463
|
+
})),
|
|
464
|
+
VRequired("extract.height", VNumber("extract.height", {
|
|
465
|
+
min: 1,
|
|
466
|
+
max: MAX_EXTRACT,
|
|
467
|
+
integer: true
|
|
468
|
+
}))
|
|
194
469
|
],
|
|
195
|
-
apply: (_context, pipe, left, top, width
|
|
470
|
+
apply: (_context, pipe, left, top, width, height) => {
|
|
196
471
|
return pipe.extract({
|
|
197
472
|
left,
|
|
198
473
|
top,
|
|
199
|
-
width
|
|
200
|
-
height
|
|
474
|
+
width,
|
|
475
|
+
height
|
|
201
476
|
});
|
|
202
477
|
}
|
|
203
478
|
};
|
|
204
479
|
const rotate = {
|
|
205
|
-
args: [
|
|
206
|
-
|
|
207
|
-
|
|
480
|
+
args: [VNumber("rotate", {
|
|
481
|
+
min: -3600,
|
|
482
|
+
max: 3600
|
|
483
|
+
})],
|
|
484
|
+
apply: (context, pipe, angle) => {
|
|
485
|
+
return pipe.rotate(angle, { background: context.background });
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const autoorient = {
|
|
489
|
+
args: [],
|
|
490
|
+
apply: (_context, pipe) => {
|
|
491
|
+
return pipe.autoOrient();
|
|
208
492
|
}
|
|
209
493
|
};
|
|
210
494
|
const flip = {
|
|
@@ -221,98 +505,243 @@ const flop = {
|
|
|
221
505
|
};
|
|
222
506
|
const sharpen = {
|
|
223
507
|
args: [
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
508
|
+
VNumber("sharpen.sigma", {
|
|
509
|
+
min: 1e-6,
|
|
510
|
+
max: 10
|
|
511
|
+
}),
|
|
512
|
+
VNumber("sharpen.flat", {
|
|
513
|
+
min: 0,
|
|
514
|
+
max: 1e6
|
|
515
|
+
}),
|
|
516
|
+
VNumber("sharpen.jagged", {
|
|
517
|
+
min: 0,
|
|
518
|
+
max: 1e6
|
|
519
|
+
}),
|
|
520
|
+
VNumber("sharpen.x1", {
|
|
521
|
+
min: 0,
|
|
522
|
+
max: 1e6
|
|
523
|
+
}),
|
|
524
|
+
VNumber("sharpen.y2", {
|
|
525
|
+
min: 0,
|
|
526
|
+
max: 1e6
|
|
527
|
+
}),
|
|
528
|
+
VNumber("sharpen.y3", {
|
|
529
|
+
min: 0,
|
|
530
|
+
max: 1e6
|
|
531
|
+
})
|
|
227
532
|
],
|
|
228
|
-
apply: (_context, pipe, sigma, flat, jagged) => {
|
|
229
|
-
return pipe.sharpen(
|
|
533
|
+
apply: (_context, pipe, sigma, flat, jagged, x1, y2, y3) => {
|
|
534
|
+
return sigma === void 0 ? pipe.sharpen() : pipe.sharpen({
|
|
535
|
+
sigma,
|
|
536
|
+
m1: flat,
|
|
537
|
+
m2: jagged,
|
|
538
|
+
x1,
|
|
539
|
+
y2,
|
|
540
|
+
y3
|
|
541
|
+
});
|
|
230
542
|
}
|
|
231
543
|
};
|
|
232
544
|
const median = {
|
|
233
|
-
args: [
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
],
|
|
545
|
+
args: [VNumber("median", {
|
|
546
|
+
min: 1,
|
|
547
|
+
max: 1e3,
|
|
548
|
+
integer: true
|
|
549
|
+
})],
|
|
238
550
|
apply: (_context, pipe, size) => {
|
|
239
551
|
return pipe.median(size);
|
|
240
552
|
}
|
|
241
553
|
};
|
|
242
554
|
const blur = {
|
|
243
555
|
args: [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
556
|
+
VNumber("blur.sigma", {
|
|
557
|
+
min: .3,
|
|
558
|
+
max: 1e3
|
|
559
|
+
}),
|
|
560
|
+
VEnum("blur.precision", [
|
|
561
|
+
"integer",
|
|
562
|
+
"float",
|
|
563
|
+
"approximate"
|
|
564
|
+
]),
|
|
565
|
+
VNumber("blur.minAmplitude", {
|
|
566
|
+
min: .001,
|
|
567
|
+
max: 1
|
|
568
|
+
})
|
|
247
569
|
],
|
|
248
|
-
apply: (_context, pipe, sigma) => {
|
|
249
|
-
return pipe.blur(
|
|
570
|
+
apply: (_context, pipe, sigma, precision, minAmplitude) => {
|
|
571
|
+
return sigma === void 0 ? pipe.blur() : pipe.blur({
|
|
572
|
+
sigma,
|
|
573
|
+
...precision === void 0 ? {} : { precision },
|
|
574
|
+
...minAmplitude === void 0 ? {} : { minAmplitude }
|
|
575
|
+
});
|
|
250
576
|
}
|
|
251
577
|
};
|
|
252
|
-
const
|
|
578
|
+
const MAX_MORPHOLOGY = 100;
|
|
579
|
+
const dilate = {
|
|
580
|
+
args: [VNumber("dilate", {
|
|
581
|
+
min: 1,
|
|
582
|
+
max: MAX_MORPHOLOGY,
|
|
583
|
+
integer: true
|
|
584
|
+
})],
|
|
585
|
+
apply: (_context, pipe, width) => {
|
|
586
|
+
return pipe.dilate(width);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const erode = {
|
|
590
|
+
args: [VNumber("erode", {
|
|
591
|
+
min: 1,
|
|
592
|
+
max: MAX_MORPHOLOGY,
|
|
593
|
+
integer: true
|
|
594
|
+
})],
|
|
595
|
+
apply: (_context, pipe, width) => {
|
|
596
|
+
return pipe.erode(width);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const MAX_CLAHE = 100;
|
|
600
|
+
const clahe = {
|
|
253
601
|
args: [
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
602
|
+
VRequired("clahe.width", VNumber("clahe.width", {
|
|
603
|
+
min: 1,
|
|
604
|
+
max: MAX_CLAHE,
|
|
605
|
+
integer: true
|
|
606
|
+
})),
|
|
607
|
+
VNumber("clahe.height", {
|
|
608
|
+
min: 1,
|
|
609
|
+
max: MAX_CLAHE,
|
|
610
|
+
integer: true
|
|
611
|
+
}),
|
|
612
|
+
VNumber("clahe.maxSlope", {
|
|
613
|
+
min: 0,
|
|
614
|
+
max: 100,
|
|
615
|
+
integer: true
|
|
616
|
+
})
|
|
257
617
|
],
|
|
618
|
+
apply: (context, pipe, width, height, maxSlope) => {
|
|
619
|
+
return pipe.clahe({
|
|
620
|
+
width: Math.min(width, context.meta.width || width),
|
|
621
|
+
height: Math.min(height ?? width, (context.meta.height || height) ?? width),
|
|
622
|
+
...maxSlope === void 0 ? {} : { maxSlope }
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const flatten = {
|
|
627
|
+
args: [],
|
|
258
628
|
apply: (context, pipe) => {
|
|
259
629
|
return pipe.flatten({ background: context.background });
|
|
260
630
|
}
|
|
261
631
|
};
|
|
632
|
+
const unflatten = {
|
|
633
|
+
args: [],
|
|
634
|
+
apply: (_context, pipe) => {
|
|
635
|
+
return pipe.unflatten();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
262
638
|
const gamma = {
|
|
263
|
-
args: [
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
639
|
+
args: [VNumber("gamma", {
|
|
640
|
+
min: 1,
|
|
641
|
+
max: 3
|
|
642
|
+
}), VNumber("gamma.gammaOut", {
|
|
643
|
+
min: 1,
|
|
644
|
+
max: 3
|
|
645
|
+
})],
|
|
646
|
+
apply: (_context, pipe, gamma, gammaOut) => {
|
|
647
|
+
return pipe.gamma(gamma, gammaOut);
|
|
270
648
|
}
|
|
271
649
|
};
|
|
272
650
|
const negate = {
|
|
273
|
-
args: [
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
VArg
|
|
277
|
-
],
|
|
278
|
-
apply: (_context, pipe) => {
|
|
279
|
-
return pipe.negate();
|
|
651
|
+
args: [VBoolean("negate.alpha")],
|
|
652
|
+
apply: (_context, pipe, alpha) => {
|
|
653
|
+
return alpha === void 0 ? pipe.negate() : pipe.negate({ alpha });
|
|
280
654
|
}
|
|
281
655
|
};
|
|
282
656
|
const normalize = {
|
|
283
|
-
args: [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
657
|
+
args: [VNumber("normalize.lower", {
|
|
658
|
+
min: 0,
|
|
659
|
+
max: 99
|
|
660
|
+
}), VNumber("normalize.upper", {
|
|
661
|
+
min: 1,
|
|
662
|
+
max: 100
|
|
663
|
+
})],
|
|
664
|
+
apply: (_context, pipe, lower, upper) => {
|
|
665
|
+
return pipe.normalize({
|
|
666
|
+
lower,
|
|
667
|
+
upper
|
|
668
|
+
});
|
|
290
669
|
}
|
|
291
670
|
};
|
|
292
671
|
const threshold = {
|
|
293
|
-
args: [
|
|
294
|
-
|
|
295
|
-
|
|
672
|
+
args: [VNumber("threshold", {
|
|
673
|
+
min: 0,
|
|
674
|
+
max: 255,
|
|
675
|
+
integer: true
|
|
676
|
+
}), VBoolean("threshold.grayscale")],
|
|
677
|
+
apply: (_context, pipe, threshold, grayscale) => {
|
|
678
|
+
return grayscale === void 0 ? pipe.threshold(threshold) : pipe.threshold(threshold, { grayscale });
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const linear = {
|
|
682
|
+
args: [VNumber("linear.a"), VNumber("linear.b")],
|
|
683
|
+
apply: (_context, pipe, a, b) => {
|
|
684
|
+
return pipe.linear(a, b);
|
|
296
685
|
}
|
|
297
686
|
};
|
|
298
687
|
const modulate = {
|
|
299
|
-
args: [
|
|
300
|
-
|
|
688
|
+
args: [
|
|
689
|
+
VNumber("modulate.brightness", { min: 0 }),
|
|
690
|
+
VNumber("modulate.saturation", { min: 0 }),
|
|
691
|
+
VNumber("modulate.hue", { integer: true }),
|
|
692
|
+
VNumber("modulate.lightness")
|
|
693
|
+
],
|
|
694
|
+
apply: (_context, pipe, brightness, saturation, hue, lightness) => {
|
|
301
695
|
return pipe.modulate({
|
|
302
|
-
brightness,
|
|
303
|
-
saturation,
|
|
304
|
-
hue
|
|
696
|
+
...brightness === void 0 ? {} : { brightness },
|
|
697
|
+
...saturation === void 0 ? {} : { saturation },
|
|
698
|
+
...hue === void 0 ? {} : { hue },
|
|
699
|
+
...lightness === void 0 ? {} : { lightness }
|
|
305
700
|
});
|
|
306
701
|
}
|
|
307
702
|
};
|
|
703
|
+
const brightness = {
|
|
704
|
+
args: [VRequired("brightness", VNumber("brightness", { min: 0 }))],
|
|
705
|
+
apply: (_context, pipe, brightness) => {
|
|
706
|
+
return pipe.modulate({ brightness });
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const saturation = {
|
|
710
|
+
args: [VRequired("saturation", VNumber("saturation", { min: 0 }))],
|
|
711
|
+
apply: (_context, pipe, saturation) => {
|
|
712
|
+
return pipe.modulate({ saturation });
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const hue = {
|
|
716
|
+
args: [VRequired("hue", VNumber("hue", { integer: true }))],
|
|
717
|
+
apply: (_context, pipe, hue) => {
|
|
718
|
+
return pipe.modulate({ hue });
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const lightness = {
|
|
722
|
+
args: [VRequired("lightness", VNumber("lightness"))],
|
|
723
|
+
apply: (_context, pipe, lightness) => {
|
|
724
|
+
return pipe.modulate({ lightness });
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const opacity = {
|
|
728
|
+
args: [VRequired("opacity", VNumber("opacity", {
|
|
729
|
+
min: 0,
|
|
730
|
+
max: 1
|
|
731
|
+
}))],
|
|
732
|
+
apply: (context, pipe, opacity) => {
|
|
733
|
+
const overlay = opacityOverlay(opacity, context.background);
|
|
734
|
+
return (context.background ? pipe : pipe.ensureAlpha()).composite([overlay]);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
308
737
|
const tint = {
|
|
309
|
-
args: [
|
|
738
|
+
args: [VColor("tint")],
|
|
310
739
|
apply: (_context, pipe, rgb) => {
|
|
311
740
|
return pipe.tint(rgb);
|
|
312
741
|
}
|
|
313
742
|
};
|
|
314
743
|
const grayscale = {
|
|
315
|
-
args: [
|
|
744
|
+
args: [],
|
|
316
745
|
apply: (_context, pipe) => {
|
|
317
746
|
return pipe.grayscale();
|
|
318
747
|
}
|
|
@@ -324,12 +753,94 @@ const w = width;
|
|
|
324
753
|
const h = height;
|
|
325
754
|
const s = resize;
|
|
326
755
|
const pos = position;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
756
|
+
const UNSAFE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
757
|
+
"script",
|
|
758
|
+
"foreignobject",
|
|
759
|
+
"iframe",
|
|
760
|
+
"embed",
|
|
761
|
+
"object",
|
|
762
|
+
"handler",
|
|
763
|
+
"listener",
|
|
764
|
+
"base",
|
|
765
|
+
"link",
|
|
766
|
+
"meta"
|
|
767
|
+
]);
|
|
768
|
+
const ANIMATION_ELEMENTS = /* @__PURE__ */ new Set([
|
|
769
|
+
"animate",
|
|
770
|
+
"animatemotion",
|
|
771
|
+
"animatetransform",
|
|
772
|
+
"set"
|
|
773
|
+
]);
|
|
774
|
+
const ANIMATION_VALUE_ATTRS = [
|
|
775
|
+
"from",
|
|
776
|
+
"to",
|
|
777
|
+
"by",
|
|
778
|
+
"values"
|
|
779
|
+
];
|
|
780
|
+
const URI_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
781
|
+
const SAFE_URI_SCHEMES = /* @__PURE__ */ new Set([
|
|
782
|
+
"http",
|
|
783
|
+
"https",
|
|
784
|
+
"mailto",
|
|
785
|
+
"tel",
|
|
786
|
+
"ftp"
|
|
787
|
+
]);
|
|
788
|
+
const SAFE_DATA_URI_RE = /^data:image\/(?!svg\+xml)[\w.+-]+[,;]/;
|
|
789
|
+
const URI_SCHEME_RE = /^([a-z][\d+.a-z-]*):/;
|
|
790
|
+
const URI_IGNORED_CHARS_RE = /[\u0000-\u0020]/g;
|
|
791
|
+
function localName(name) {
|
|
792
|
+
const colonIndex = name.indexOf(":");
|
|
793
|
+
return (colonIndex === -1 ? name : name.slice(colonIndex + 1)).toLowerCase();
|
|
794
|
+
}
|
|
795
|
+
function isUnsafeURI(value) {
|
|
796
|
+
const uri = value.replace(URI_IGNORED_CHARS_RE, "").toLowerCase();
|
|
797
|
+
const scheme = URI_SCHEME_RE.exec(uri)?.[1];
|
|
798
|
+
if (!scheme) return false;
|
|
799
|
+
if (scheme === "data") return !SAFE_DATA_URI_RE.test(uri);
|
|
800
|
+
return !SAFE_URI_SCHEMES.has(scheme);
|
|
801
|
+
}
|
|
802
|
+
function isUnsafeAnimation(attributes) {
|
|
803
|
+
const attrs = {};
|
|
804
|
+
for (const [name, value] of Object.entries(attributes)) attrs[localName(name)] = value;
|
|
805
|
+
if (localName(attrs.attributename?.trim() || "").startsWith("on")) return true;
|
|
806
|
+
return ANIMATION_VALUE_ATTRS.some((attr) => (attrs[attr] || "").split(";").some((value) => isUnsafeURI(value)));
|
|
807
|
+
}
|
|
808
|
+
function detach(node, parent) {
|
|
809
|
+
parent.children = parent.children.filter((child) => child !== node);
|
|
810
|
+
}
|
|
811
|
+
const sanitizeSVGPlugin = {
|
|
812
|
+
name: "ipx-sanitize",
|
|
813
|
+
fn: () => ({
|
|
814
|
+
doctype: { enter: (node, parentNode) => detach(node, parentNode) },
|
|
815
|
+
instruction: { enter: (node, parentNode) => detach(node, parentNode) },
|
|
816
|
+
element: { enter: (node, parentNode) => {
|
|
817
|
+
const name = localName(node.name);
|
|
818
|
+
if (UNSAFE_ELEMENTS.has(name)) {
|
|
819
|
+
detach(node, parentNode);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (ANIMATION_ELEMENTS.has(name) && isUnsafeAnimation(node.attributes)) {
|
|
823
|
+
detach(node, parentNode);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
for (const [attr, value] of Object.entries(node.attributes)) {
|
|
827
|
+
const attrName = localName(attr);
|
|
828
|
+
if (attrName.startsWith("on") || URI_ATTRS.has(attrName) && isUnsafeURI(value)) delete node.attributes[attr];
|
|
829
|
+
}
|
|
830
|
+
} }
|
|
831
|
+
})
|
|
832
|
+
};
|
|
330
833
|
function getEnv(name) {
|
|
331
834
|
const value = globalThis.process?.env?.[name];
|
|
332
|
-
if (value
|
|
835
|
+
if (value === void 0) return;
|
|
836
|
+
try {
|
|
837
|
+
return JSON.parse(value);
|
|
838
|
+
} catch {
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function getBuiltinModule(id) {
|
|
843
|
+
return globalThis.process?.getBuiltinModule?.(id);
|
|
333
844
|
}
|
|
334
845
|
function requireModule(id) {
|
|
335
846
|
const { createRequire } = globalThis.process.getBuiltinModule("node:module");
|
|
@@ -343,10 +854,8 @@ function cachedPromise(function_) {
|
|
|
343
854
|
return p;
|
|
344
855
|
};
|
|
345
856
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//#region src/ipx.ts
|
|
349
|
-
const SUPPORTED_FORMATS = new Set([
|
|
857
|
+
const DEFAULT_MAX_OUTPUT_DIMENSION = 8192;
|
|
858
|
+
const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
350
859
|
"jpeg",
|
|
351
860
|
"png",
|
|
352
861
|
"webp",
|
|
@@ -356,17 +865,12 @@ const SUPPORTED_FORMATS = new Set([
|
|
|
356
865
|
"gif",
|
|
357
866
|
"heic"
|
|
358
867
|
]);
|
|
359
|
-
/**
|
|
360
|
-
* Creates an IPX image processing instance with the specified options.
|
|
361
|
-
* @param {IPXOptions} userOptions - Configuration options for the IPX instance. See {@link IPXOptions}.
|
|
362
|
-
* @returns {IPX} An IPX processing function configured with the given options. See {@link IPX}.
|
|
363
|
-
* @throws {Error} If critical options such as storage are missing or incorrectly configured.
|
|
364
|
-
*/
|
|
365
868
|
function createIPX(userOptions) {
|
|
366
869
|
const options = {
|
|
367
870
|
...userOptions,
|
|
368
871
|
alias: userOptions.alias || getEnv("IPX_ALIAS") || {},
|
|
369
872
|
maxAge: userOptions.maxAge ?? getEnv("IPX_MAX_AGE") ?? 60,
|
|
873
|
+
maxOutputDimension: userOptions.maxOutputDimension ?? getEnv("IPX_MAX_OUTPUT_DIMENSION") ?? DEFAULT_MAX_OUTPUT_DIMENSION,
|
|
370
874
|
sharpOptions: {
|
|
371
875
|
jpegProgressive: true,
|
|
372
876
|
...userOptions.sharpOptions
|
|
@@ -377,7 +881,7 @@ function createIPX(userOptions) {
|
|
|
377
881
|
return await import("sharp").then((r) => r.default || r);
|
|
378
882
|
});
|
|
379
883
|
const getSVGO = cachedPromise(async () => {
|
|
380
|
-
const { optimize } = await import("./svgo-node.mjs").then(
|
|
884
|
+
const { optimize } = await import("./svgo-node.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
381
885
|
return { optimize };
|
|
382
886
|
});
|
|
383
887
|
return function ipx(id, modifiers = {}, opts = {}) {
|
|
@@ -432,25 +936,43 @@ function createIPX(userOptions) {
|
|
|
432
936
|
}
|
|
433
937
|
let mFormat = modifiers.f || modifiers.format;
|
|
434
938
|
if (mFormat === "jpg") mFormat = "jpeg";
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
939
|
+
if (imageMeta$1.type === "svg" && (!mFormat || mFormat === "svg")) {
|
|
940
|
+
const sanitize = !options.svg?.unsafeSkipSanitize;
|
|
941
|
+
const shouldOptimize = options.svg?.optimize !== false;
|
|
942
|
+
const svgoConfig = options.svg?.optimize === false ? void 0 : options.svg?.optimize;
|
|
943
|
+
if (!sanitize && !shouldOptimize) return {
|
|
944
|
+
data: sourceData,
|
|
945
|
+
format: "svg+xml",
|
|
946
|
+
meta: imageMeta$1
|
|
947
|
+
};
|
|
948
|
+
const plugins = sanitize ? ["removeScripts", sanitizeSVGPlugin] : [];
|
|
949
|
+
if (svgoConfig?.plugins) plugins.push(...svgoConfig.plugins);
|
|
950
|
+
else if (shouldOptimize) plugins.push("preset-default");
|
|
442
951
|
const { optimize } = await getSVGO();
|
|
952
|
+
let svg;
|
|
953
|
+
try {
|
|
954
|
+
svg = optimize(sourceData.toString("utf8"), {
|
|
955
|
+
...svgoConfig,
|
|
956
|
+
plugins
|
|
957
|
+
}).data;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
throw new HTTPError({
|
|
960
|
+
statusCode: 400,
|
|
961
|
+
statusText: `IPX_INVALID_SVG`,
|
|
962
|
+
message: `Cannot parse SVG: ${id}`,
|
|
963
|
+
cause: error
|
|
964
|
+
});
|
|
965
|
+
}
|
|
443
966
|
return {
|
|
444
|
-
data:
|
|
445
|
-
...options.svgo,
|
|
446
|
-
plugins: ["removeScripts", ...options.svgo?.plugins || []]
|
|
447
|
-
}).data,
|
|
967
|
+
data: svg,
|
|
448
968
|
format: "svg+xml",
|
|
449
969
|
meta: imageMeta$1
|
|
450
970
|
};
|
|
451
971
|
}
|
|
972
|
+
const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
|
|
452
973
|
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
453
|
-
|
|
974
|
+
const Sharp = await getSharp();
|
|
975
|
+
let sharp = Sharp(sourceData, {
|
|
454
976
|
animated,
|
|
455
977
|
...options.sharpOptions
|
|
456
978
|
});
|
|
@@ -459,16 +981,35 @@ function createIPX(userOptions) {
|
|
|
459
981
|
handler: getHandler(name),
|
|
460
982
|
name,
|
|
461
983
|
args: arguments_
|
|
462
|
-
})).filter((h
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
return
|
|
984
|
+
})).filter((h) => h.handler).sort((a, b) => {
|
|
985
|
+
const aOrder = a.handler.order || 0;
|
|
986
|
+
const bOrder = b.handler.order || 0;
|
|
987
|
+
return aOrder === bOrder ? (a.name || "").localeCompare(b.name || "") : aOrder - bOrder;
|
|
466
988
|
});
|
|
467
|
-
const handlerContext = {
|
|
468
|
-
|
|
989
|
+
const handlerContext = {
|
|
990
|
+
meta: imageMeta$1,
|
|
991
|
+
maxOutputDimension: options.maxOutputDimension
|
|
992
|
+
};
|
|
993
|
+
for (const h of handlers) sharp = applyHandler(handlerContext, sharp, h.handler, h.args.toString()) || sharp;
|
|
994
|
+
if (handlerContext.background) try {
|
|
995
|
+
Sharp({ create: {
|
|
996
|
+
width: 1,
|
|
997
|
+
height: 1,
|
|
998
|
+
channels: 4,
|
|
999
|
+
background: handlerContext.background
|
|
1000
|
+
} });
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
throw asModifierError(error);
|
|
1003
|
+
}
|
|
469
1004
|
if (SUPPORTED_FORMATS.has(format || "")) sharp = sharp.toFormat(format, { quality: handlerContext.quality });
|
|
1005
|
+
let processedImage;
|
|
1006
|
+
try {
|
|
1007
|
+
processedImage = await sharp.toBuffer();
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw asModifierError(error);
|
|
1010
|
+
}
|
|
470
1011
|
return {
|
|
471
|
-
data:
|
|
1012
|
+
data: processedImage,
|
|
472
1013
|
format,
|
|
473
1014
|
meta: imageMeta$1
|
|
474
1015
|
};
|
|
@@ -476,47 +1017,56 @@ function createIPX(userOptions) {
|
|
|
476
1017
|
};
|
|
477
1018
|
};
|
|
478
1019
|
}
|
|
479
|
-
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/server.ts
|
|
482
1020
|
var import_etag = /* @__PURE__ */ __toESM(require_etag(), 1);
|
|
483
1021
|
var import_accept_negotiator = require_accept_negotiator();
|
|
484
|
-
function createIPXFetchHandler(ipx) {
|
|
485
|
-
return createIPXHandler(ipx).fetch;
|
|
1022
|
+
function createIPXFetchHandler(ipx, opts) {
|
|
1023
|
+
return createIPXHandler(ipx, opts).fetch;
|
|
486
1024
|
}
|
|
487
|
-
function createIPXNodeHandler(ipx) {
|
|
1025
|
+
function createIPXNodeHandler(ipx, opts) {
|
|
488
1026
|
const { toNodeHandler } = requireModule("srvx/node");
|
|
489
|
-
return toNodeHandler(createIPXFetchHandler(ipx));
|
|
1027
|
+
return toNodeHandler(createIPXFetchHandler(ipx, opts));
|
|
490
1028
|
}
|
|
491
1029
|
function serveIPX(ipx, opts) {
|
|
492
1030
|
const { serve } = requireModule("srvx");
|
|
493
|
-
const
|
|
1031
|
+
const { parseURL, ...serverOptions } = opts || {};
|
|
1032
|
+
const fetch = createIPXFetchHandler(ipx, { parseURL });
|
|
494
1033
|
return serve({
|
|
495
|
-
...
|
|
496
|
-
fetch
|
|
1034
|
+
...serverOptions,
|
|
1035
|
+
fetch
|
|
497
1036
|
});
|
|
498
1037
|
}
|
|
499
1038
|
const MODIFIER_SEP = /[&,]/g;
|
|
500
1039
|
const MODIFIER_VAL_SEP = /[:=_]/;
|
|
501
|
-
function
|
|
1040
|
+
function parseIPXURL(url) {
|
|
1041
|
+
const [modifiersString = "", ...idSegments] = new URL(url).pathname.slice(1).split("/");
|
|
1042
|
+
const id = decode$1(idSegments.join("/"));
|
|
1043
|
+
if (!modifiersString) throw new HTTPError({
|
|
1044
|
+
statusCode: 400,
|
|
1045
|
+
statusText: "IPX_MISSING_MODIFIERS",
|
|
1046
|
+
message: `Modifiers are missing: ${safeString(id)}`
|
|
1047
|
+
});
|
|
1048
|
+
const modifiers = Object.create(null);
|
|
1049
|
+
if (modifiersString !== "_") for (const p of modifiersString.split(MODIFIER_SEP)) {
|
|
1050
|
+
const [key = "", ...values] = p.split(MODIFIER_VAL_SEP);
|
|
1051
|
+
modifiers[key] = values.map((v) => decode$1(v)).join("_");
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
id,
|
|
1055
|
+
modifiers
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function createIPXHandler(ipx, opts = {}) {
|
|
1059
|
+
const parseURL = opts.parseURL || parseIPXURL;
|
|
502
1060
|
return defineEventHandler(async (event) => {
|
|
503
|
-
const
|
|
504
|
-
const id = safeString(
|
|
505
|
-
if (!modifiersString) throw new HTTPError({
|
|
506
|
-
statusCode: 400,
|
|
507
|
-
statusText: "IPX_MISSING_MODIFIERS",
|
|
508
|
-
message: `Modifiers are missing: ${id}`
|
|
509
|
-
});
|
|
1061
|
+
const parsed = await parseURL(event.req.url) || {};
|
|
1062
|
+
const id = safeString(parsed.id);
|
|
510
1063
|
if (!id || id === "/") throw new HTTPError({
|
|
511
1064
|
statusCode: 400,
|
|
512
1065
|
statusText: "IPX_MISSING_ID",
|
|
513
1066
|
message: `Resource id is missing: ${event.path}`
|
|
514
1067
|
});
|
|
515
1068
|
const modifiers = Object.create(null);
|
|
516
|
-
|
|
517
|
-
const [key, ...values] = p.split(MODIFIER_VAL_SEP);
|
|
518
|
-
modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
|
|
519
|
-
}
|
|
1069
|
+
for (const [key, value] of Object.entries(parsed.modifiers || {})) modifiers[safeString(key)] = safeString(value);
|
|
520
1070
|
if ((modifiers.f || modifiers.format) === "auto") {
|
|
521
1071
|
const acceptHeader = event.req.headers.get("accept") || "";
|
|
522
1072
|
const animated = modifiers.animated ?? modifiers.a;
|
|
@@ -531,6 +1081,9 @@ function createIPXHandler(ipx) {
|
|
|
531
1081
|
const img = ipx(id, modifiers);
|
|
532
1082
|
const sourceMeta = await img.getSourceMeta();
|
|
533
1083
|
sendResponseHeaderIfNotSet(event, "content-security-policy", "default-src 'none'");
|
|
1084
|
+
sendResponseHeaderIfNotSet(event, "x-content-type-options", "nosniff");
|
|
1085
|
+
if (typeof sourceMeta.maxAge === "number") sendResponseHeaderIfNotSet(event, "cache-control", `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`);
|
|
1086
|
+
const ifNoneMatch = event.req.headers.get("if-none-match");
|
|
534
1087
|
if (sourceMeta.mtime) {
|
|
535
1088
|
sendResponseHeaderIfNotSet(event, "last-modified", sourceMeta.mtime.toUTCString());
|
|
536
1089
|
const _ifModifiedSince = event.req.headers.get("if-modified-since");
|
|
@@ -539,13 +1092,22 @@ function createIPXHandler(ipx) {
|
|
|
539
1092
|
return;
|
|
540
1093
|
}
|
|
541
1094
|
}
|
|
1095
|
+
const weakEtag = sourceMeta.mtime ? getWeakEtag(id, modifiers, sourceMeta.mtime) : void 0;
|
|
1096
|
+
if (weakEtag) {
|
|
1097
|
+
sendResponseHeaderIfNotSet(event, "etag", weakEtag);
|
|
1098
|
+
if (etagMatches(ifNoneMatch, weakEtag)) {
|
|
1099
|
+
event.res.status = 304;
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
542
1103
|
const { data, format } = await img.process();
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1104
|
+
if (!weakEtag) {
|
|
1105
|
+
const etag = (0, import_etag.default)(data);
|
|
1106
|
+
sendResponseHeaderIfNotSet(event, "etag", etag);
|
|
1107
|
+
if (etagMatches(ifNoneMatch, etag)) {
|
|
1108
|
+
event.res.status = 304;
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
549
1111
|
}
|
|
550
1112
|
if (format) sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
|
|
551
1113
|
return data;
|
|
@@ -554,6 +1116,25 @@ function createIPXHandler(ipx) {
|
|
|
554
1116
|
function sendResponseHeaderIfNotSet(event, name, value) {
|
|
555
1117
|
if (!event.res.headers.has(name)) event.res.headers.set(name, value);
|
|
556
1118
|
}
|
|
1119
|
+
function getWeakEtag(id, modifiers, mtime) {
|
|
1120
|
+
const crypto = getBuiltinModule("node:crypto");
|
|
1121
|
+
if (!crypto?.createHash) return;
|
|
1122
|
+
const key = JSON.stringify([
|
|
1123
|
+
mtime.getTime(),
|
|
1124
|
+
id,
|
|
1125
|
+
Object.keys(modifiers).sort().map((key) => [key, modifiers[key]])
|
|
1126
|
+
]);
|
|
1127
|
+
return `W/"${crypto.createHash("sha256").update(key).digest("base64url").slice(0, 27)}"`;
|
|
1128
|
+
}
|
|
1129
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
1130
|
+
if (!ifNoneMatch || !etag) return false;
|
|
1131
|
+
if (ifNoneMatch === "*") return true;
|
|
1132
|
+
const opaque = opaqueTag(etag);
|
|
1133
|
+
return ifNoneMatch.split(",").some((tag) => opaqueTag(tag.trim()) === opaque);
|
|
1134
|
+
}
|
|
1135
|
+
function opaqueTag(tag) {
|
|
1136
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
1137
|
+
}
|
|
557
1138
|
function autoDetectFormat(acceptHeader, animated) {
|
|
558
1139
|
if (animated) return (0, import_accept_negotiator.negotiate)(acceptHeader, ["image/webp", "image/gif"])?.split("/")[1] || "gif";
|
|
559
1140
|
return (0, import_accept_negotiator.negotiate)(acceptHeader, [
|
|
@@ -566,44 +1147,225 @@ function autoDetectFormat(acceptHeader, animated) {
|
|
|
566
1147
|
"image/gif"
|
|
567
1148
|
])?.split("/")[1] || "jpeg";
|
|
568
1149
|
}
|
|
1150
|
+
function decode$1(input) {
|
|
1151
|
+
try {
|
|
1152
|
+
return decodeURIComponent(input);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return input;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
569
1157
|
function safeString(input) {
|
|
570
|
-
return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
1158
|
+
return JSON.stringify(input ?? "").replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
571
1159
|
}
|
|
572
|
-
|
|
573
|
-
//#endregion
|
|
574
|
-
//#region src/storage/http.ts
|
|
575
1160
|
const HTTP_RE = /^https?:\/\//;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1161
|
+
const REDIRECT_STATUS = /* @__PURE__ */ new Set([
|
|
1162
|
+
301,
|
|
1163
|
+
302,
|
|
1164
|
+
303,
|
|
1165
|
+
307,
|
|
1166
|
+
308
|
|
1167
|
+
]);
|
|
1168
|
+
const MAX_REDIRECTS = 3;
|
|
1169
|
+
function parseIPv4(input) {
|
|
1170
|
+
const parts = input.split(".");
|
|
1171
|
+
if (parts.length !== 4) return;
|
|
1172
|
+
const octets = [];
|
|
1173
|
+
for (const part of parts) {
|
|
1174
|
+
if (!/^\d{1,3}$/.test(part)) return;
|
|
1175
|
+
const octet = Number(part);
|
|
1176
|
+
if (octet > 255) return;
|
|
1177
|
+
octets.push(octet);
|
|
1178
|
+
}
|
|
1179
|
+
return octets;
|
|
1180
|
+
}
|
|
1181
|
+
function parseIPv6(input) {
|
|
1182
|
+
const zoneIndex = input.indexOf("%");
|
|
1183
|
+
const halves = (zoneIndex === -1 ? input : input.slice(0, zoneIndex)).split("::");
|
|
1184
|
+
if (halves.length > 2) return;
|
|
1185
|
+
const expand = (part) => {
|
|
1186
|
+
if (part === "") return [];
|
|
1187
|
+
const bytes = [];
|
|
1188
|
+
const groups = part.split(":");
|
|
1189
|
+
for (const [index, group] of groups.entries()) {
|
|
1190
|
+
if (group.includes(".")) {
|
|
1191
|
+
if (index !== groups.length - 1) return;
|
|
1192
|
+
const octets = parseIPv4(group);
|
|
1193
|
+
if (!octets) return;
|
|
1194
|
+
bytes.push(...octets);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (!/^[\da-f]{1,4}$/i.test(group)) return;
|
|
1198
|
+
const value = Number.parseInt(group, 16);
|
|
1199
|
+
bytes.push(value >> 8, value & 255);
|
|
1200
|
+
}
|
|
1201
|
+
return bytes;
|
|
1202
|
+
};
|
|
1203
|
+
const head = expand(halves[0]);
|
|
1204
|
+
const tail = halves.length === 2 ? expand(halves[1]) : [];
|
|
1205
|
+
if (!head || !tail) return;
|
|
1206
|
+
if (halves.length === 1) return head.length === 16 ? head : void 0;
|
|
1207
|
+
const fill = 16 - head.length - tail.length;
|
|
1208
|
+
if (fill < 0) return;
|
|
1209
|
+
return [
|
|
1210
|
+
...head,
|
|
1211
|
+
...Array.from({ length: fill }, () => 0),
|
|
1212
|
+
...tail
|
|
1213
|
+
];
|
|
1214
|
+
}
|
|
1215
|
+
function embeddedIPv4(bytes) {
|
|
1216
|
+
if (bytes.slice(0, 10).every((byte) => byte === 0) && (bytes[10] === 255 && bytes[11] === 255 || bytes[10] === 0 && bytes[11] === 0)) return bytes.slice(12);
|
|
1217
|
+
if (bytes.slice(0, 8).every((byte) => byte === 0) && bytes[8] === 255 && bytes[9] === 255 && bytes[10] === 0 && bytes[11] === 0) return bytes.slice(12);
|
|
1218
|
+
if (bytes[0] === 0 && bytes[1] === 100 && bytes[2] === 255 && bytes[3] === 155 && bytes.slice(4, 12).every((byte) => byte === 0)) return bytes.slice(12);
|
|
1219
|
+
if (bytes[0] === 32 && bytes[1] === 2) return bytes.slice(2, 6);
|
|
1220
|
+
}
|
|
1221
|
+
function isPublicIPv4(octets) {
|
|
1222
|
+
const [a, b, c] = octets;
|
|
1223
|
+
return !(a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 && c === 0 || a === 192 && b === 0 && c === 2 || a === 192 && b === 88 && c === 99 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a >= 224);
|
|
1224
|
+
}
|
|
1225
|
+
function isPublicIPv6(bytes) {
|
|
1226
|
+
const embedded = embeddedIPv4(bytes);
|
|
1227
|
+
if (embedded) return isPublicIPv4(embedded);
|
|
1228
|
+
const [a, b] = bytes;
|
|
1229
|
+
return !(a === 255 || a === 254 && (b & 192) === 128 || a === 254 && (b & 192) === 192 || (a & 254) === 252 || a === 0 && b === 100 && bytes[2] === 255 && bytes[3] === 155 || a === 1 && b === 0 && bytes.slice(2, 8).every((x) => x === 0) || a === 32 && b === 1 && bytes[2] === 13 && bytes[3] === 184 || a === 32 && b === 1 && bytes[2] === 0 && bytes[3] === 0);
|
|
1230
|
+
}
|
|
1231
|
+
function isPublicIP(address, family) {
|
|
1232
|
+
if (family === 4) {
|
|
1233
|
+
const octets = parseIPv4(address);
|
|
1234
|
+
return octets ? isPublicIPv4(octets) : false;
|
|
1235
|
+
}
|
|
1236
|
+
if (family === 6) {
|
|
1237
|
+
const bytes = parseIPv6(address);
|
|
1238
|
+
return bytes ? isPublicIPv6(bytes) : false;
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
function decode(input) {
|
|
1243
|
+
try {
|
|
1244
|
+
return decodeURIComponent(input);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return input;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
584
1249
|
function ipxHttpStorage(_options = {}) {
|
|
585
1250
|
const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
|
|
586
1251
|
let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
|
|
587
1252
|
const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
|
|
588
1253
|
const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
|
|
589
|
-
|
|
1254
|
+
const blockPrivateIPs = _options.blockPrivateIPs ?? getEnv("IPX_HTTP_BLOCK_PRIVATE_IPS") ?? false;
|
|
1255
|
+
if (typeof _domains === "string") _domains = _domains.split(",").map((s) => s.trim());
|
|
590
1256
|
const domains = new Set(_domains.map((d) => {
|
|
591
1257
|
if (!HTTP_RE.test(d)) d = "http://" + d;
|
|
592
1258
|
return new URL(d).hostname;
|
|
593
1259
|
}).filter(Boolean));
|
|
594
|
-
function validateId(id) {
|
|
595
|
-
|
|
1260
|
+
async function validateId(id) {
|
|
1261
|
+
let url;
|
|
1262
|
+
try {
|
|
1263
|
+
url = new URL(decode(id));
|
|
1264
|
+
} catch {
|
|
1265
|
+
throw new HTTPError({
|
|
1266
|
+
statusCode: 400,
|
|
1267
|
+
statusText: `IPX_INVALID_URL`,
|
|
1268
|
+
message: `Invalid URL: ${id}`
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
await validateURL(url, id);
|
|
1272
|
+
return url.toString();
|
|
1273
|
+
}
|
|
1274
|
+
async function validateURL(url, id = url.toString()) {
|
|
596
1275
|
if (!url.hostname) throw new HTTPError({
|
|
597
1276
|
statusCode: 403,
|
|
598
1277
|
statusText: `IPX_MISSING_HOSTNAME`,
|
|
599
1278
|
message: `Hostname is missing: ${id}`
|
|
600
1279
|
});
|
|
1280
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new HTTPError({
|
|
1281
|
+
statusCode: 403,
|
|
1282
|
+
statusText: `IPX_FORBIDDEN_PROTOCOL`,
|
|
1283
|
+
message: `Forbidden protocol: ${url.protocol}`
|
|
1284
|
+
});
|
|
601
1285
|
if (!allowAllDomains && !domains.has(url.hostname)) throw new HTTPError({
|
|
602
1286
|
statusCode: 403,
|
|
603
1287
|
statusText: `IPX_FORBIDDEN_HOST`,
|
|
604
1288
|
message: `Forbidden host: ${url.hostname}`
|
|
605
1289
|
});
|
|
606
|
-
|
|
1290
|
+
if (blockPrivateIPs) await validatePublicIP(url, id);
|
|
1291
|
+
}
|
|
1292
|
+
async function validatePublicIP(url, id) {
|
|
1293
|
+
const hostname = url.hostname.replace(/^\[|]$/g, "");
|
|
1294
|
+
const net = getBuiltinModule("node:net");
|
|
1295
|
+
const dns = getBuiltinModule("node:dns");
|
|
1296
|
+
if (!net?.isIP || !dns?.promises?.lookup) throw new HTTPError({
|
|
1297
|
+
statusCode: 500,
|
|
1298
|
+
statusText: `IPX_IP_CHECK_UNAVAILABLE`,
|
|
1299
|
+
message: `Cannot verify the address of ${hostname}: \`blockPrivateIPs\` requires \`node:net\` and \`node:dns\`.`
|
|
1300
|
+
});
|
|
1301
|
+
const assertPublic = (address, family) => {
|
|
1302
|
+
if (!isPublicIP(address, family)) throw new HTTPError({
|
|
1303
|
+
statusCode: 403,
|
|
1304
|
+
statusText: `IPX_FORBIDDEN_IP`,
|
|
1305
|
+
message: `Forbidden IP address: ${address} (${id})`
|
|
1306
|
+
});
|
|
1307
|
+
};
|
|
1308
|
+
const family = net.isIP(hostname);
|
|
1309
|
+
if (family) {
|
|
1310
|
+
assertPublic(hostname, family);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
let addresses;
|
|
1314
|
+
try {
|
|
1315
|
+
addresses = await dns.promises.lookup(hostname, {
|
|
1316
|
+
all: true,
|
|
1317
|
+
verbatim: true
|
|
1318
|
+
});
|
|
1319
|
+
} catch {
|
|
1320
|
+
throw new HTTPError({
|
|
1321
|
+
statusCode: 502,
|
|
1322
|
+
statusText: `IPX_DNS_LOOKUP_FAILED`,
|
|
1323
|
+
message: `Cannot resolve host: ${hostname}`
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
if (addresses.length === 0) throw new HTTPError({
|
|
1327
|
+
statusCode: 502,
|
|
1328
|
+
statusText: `IPX_DNS_LOOKUP_FAILED`,
|
|
1329
|
+
message: `Cannot resolve host: ${hostname}`
|
|
1330
|
+
});
|
|
1331
|
+
for (const { address, family } of addresses) assertPublic(address, family || net.isIP(address));
|
|
1332
|
+
}
|
|
1333
|
+
async function fetchURL(url, init) {
|
|
1334
|
+
const _init = {
|
|
1335
|
+
...fetchOptions,
|
|
1336
|
+
...init
|
|
1337
|
+
};
|
|
1338
|
+
if (allowAllDomains && !blockPrivateIPs || _init.redirect) return fetch(url, _init);
|
|
1339
|
+
let currentURL = url;
|
|
1340
|
+
let method = _init.method || "GET";
|
|
1341
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
1342
|
+
const response = await fetch(currentURL, {
|
|
1343
|
+
..._init,
|
|
1344
|
+
method,
|
|
1345
|
+
redirect: "manual"
|
|
1346
|
+
});
|
|
1347
|
+
if (!REDIRECT_STATUS.has(response.status)) return response;
|
|
1348
|
+
const location = response.headers.get("location");
|
|
1349
|
+
if (!location) return response;
|
|
1350
|
+
let nextURL;
|
|
1351
|
+
try {
|
|
1352
|
+
nextURL = new URL(location, currentURL);
|
|
1353
|
+
} catch {
|
|
1354
|
+
throw new HTTPError({
|
|
1355
|
+
statusCode: 502,
|
|
1356
|
+
statusText: `IPX_INVALID_REDIRECT`,
|
|
1357
|
+
message: `Invalid redirect location: ${location}`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
await validateURL(nextURL, location);
|
|
1361
|
+
if (response.status === 303 && method !== "HEAD") method = "GET";
|
|
1362
|
+
currentURL = nextURL.toString();
|
|
1363
|
+
}
|
|
1364
|
+
throw new HTTPError({
|
|
1365
|
+
statusCode: 502,
|
|
1366
|
+
statusText: `IPX_TOO_MANY_REDIRECTS`,
|
|
1367
|
+
message: `Too many redirects (max ${MAX_REDIRECTS}): ${url}`
|
|
1368
|
+
});
|
|
607
1369
|
}
|
|
608
1370
|
function parseResponse(response) {
|
|
609
1371
|
let maxAge = defaultMaxAge;
|
|
@@ -625,12 +1387,9 @@ function ipxHttpStorage(_options = {}) {
|
|
|
625
1387
|
return {
|
|
626
1388
|
name: "ipx:http",
|
|
627
1389
|
async getMeta(id) {
|
|
628
|
-
const url = validateId(id);
|
|
1390
|
+
const url = await validateId(id);
|
|
629
1391
|
try {
|
|
630
|
-
const response = await
|
|
631
|
-
...fetchOptions,
|
|
632
|
-
method: "HEAD"
|
|
633
|
-
});
|
|
1392
|
+
const response = await fetchURL(url, { method: "HEAD" });
|
|
634
1393
|
if (!response.ok) return {};
|
|
635
1394
|
const { maxAge, mtime } = parseResponse(response);
|
|
636
1395
|
return {
|
|
@@ -642,8 +1401,7 @@ function ipxHttpStorage(_options = {}) {
|
|
|
642
1401
|
}
|
|
643
1402
|
},
|
|
644
1403
|
async getData(id) {
|
|
645
|
-
const
|
|
646
|
-
const response = await fetch(url, fetchOptions);
|
|
1404
|
+
const response = await fetchURL(await validateId(id));
|
|
647
1405
|
if (!response.ok) throw new HTTPError({
|
|
648
1406
|
statusCode: response.status,
|
|
649
1407
|
statusText: response.statusText,
|
|
@@ -653,44 +1411,66 @@ function ipxHttpStorage(_options = {}) {
|
|
|
653
1411
|
}
|
|
654
1412
|
};
|
|
655
1413
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
* @param {NodeFSSOptions} [_options={}] - File system storage configuration options, with optional directory paths and caching configuration. See {@link NodeFSSOptions}.
|
|
664
|
-
* @returns {IPXStorage} An implementation of the IPXStorage interface for accessing images stored on the local file system. See {@link IPXStorage}.
|
|
665
|
-
* @throws {H3Error} If there is a problem accessing the file system module or resolving/reading files. See {@link H3Error}.
|
|
666
|
-
*/
|
|
1414
|
+
const { join, parse, resolve, relative, isAbsolute, sep } = /* #__PURE__ */ getBuiltinModule("node:path") || {};
|
|
1415
|
+
const NOT_IN_THIS_DIR = /* @__PURE__ */ new Set([
|
|
1416
|
+
"ENOENT",
|
|
1417
|
+
"ENOTDIR",
|
|
1418
|
+
"ELOOP",
|
|
1419
|
+
"ENAMETOOLONG"
|
|
1420
|
+
]);
|
|
667
1421
|
function ipxFSStorage(_options = {}) {
|
|
1422
|
+
const fs = getBuiltinModule("node:fs/promises");
|
|
1423
|
+
if (!fs) throw new Error("ipxFSStorage requires the `node:fs/promises` builtin, which this runtime does not provide");
|
|
668
1424
|
const dirs = resolveDirs(_options.dir);
|
|
669
|
-
const maxAge = _options.maxAge
|
|
670
|
-
const
|
|
1425
|
+
const maxAge = _options.maxAge ?? getEnv("IPX_FS_MAX_AGE");
|
|
1426
|
+
const allowSymlinksOutsideDir = _options.allowSymlinksOutsideDir ?? getEnv("IPX_FS_ALLOW_SYMLINKS_OUTSIDE_DIR") ?? false;
|
|
1427
|
+
const realDirCache = [];
|
|
1428
|
+
const getRealDir = (index, dir) => {
|
|
1429
|
+
let realDir = realDirCache[index];
|
|
1430
|
+
if (!realDir) {
|
|
1431
|
+
realDir = fs.realpath(dir).catch(() => dir);
|
|
1432
|
+
realDirCache[index] = realDir;
|
|
1433
|
+
}
|
|
1434
|
+
return realDir;
|
|
1435
|
+
};
|
|
671
1436
|
const resolveFile = async (id) => {
|
|
672
|
-
for (const dir of dirs) {
|
|
1437
|
+
for (const [index, dir] of dirs.entries()) {
|
|
673
1438
|
const filePath = join(dir, id);
|
|
674
|
-
if (!isValidPath(filePath) || !filePath
|
|
1439
|
+
if (!isValidPath(filePath) || !isInsideDir(filePath, dir)) throw new HTTPError({
|
|
675
1440
|
statusCode: 403,
|
|
676
1441
|
statusText: `IPX_FORBIDDEN_PATH`,
|
|
677
1442
|
message: `Forbidden path: ${id}`
|
|
678
1443
|
});
|
|
1444
|
+
let stats;
|
|
1445
|
+
let realPath;
|
|
679
1446
|
try {
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
return {
|
|
683
|
-
stats,
|
|
684
|
-
read: () => fs.readFile(filePath)
|
|
685
|
-
};
|
|
1447
|
+
stats = await fs.stat(filePath);
|
|
1448
|
+
if (stats.isFile() && !allowSymlinksOutsideDir) realPath = await fs.realpath(filePath);
|
|
686
1449
|
} catch (error) {
|
|
687
|
-
if (error.code
|
|
1450
|
+
if (NOT_IN_THIS_DIR.has(error.code)) continue;
|
|
688
1451
|
throw new HTTPError({
|
|
689
1452
|
statusCode: 403,
|
|
690
1453
|
statusText: `IPX_FORBIDDEN_FILE`,
|
|
691
1454
|
message: `Cannot access file: ${id}`
|
|
692
1455
|
});
|
|
693
1456
|
}
|
|
1457
|
+
if (!stats.isFile()) continue;
|
|
1458
|
+
if (realPath !== void 0) {
|
|
1459
|
+
let realDir = await getRealDir(index, dir);
|
|
1460
|
+
if (!isInsideDir(realPath, realDir)) {
|
|
1461
|
+
realDirCache[index] = void 0;
|
|
1462
|
+
realDir = await getRealDir(index, dir);
|
|
1463
|
+
}
|
|
1464
|
+
if (!isInsideDir(realPath, realDir)) throw new HTTPError({
|
|
1465
|
+
statusCode: 403,
|
|
1466
|
+
statusText: `IPX_FORBIDDEN_SYMLINK`,
|
|
1467
|
+
message: `Forbidden symlink: ${id}`
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
stats,
|
|
1472
|
+
read: () => fs.readFile(realPath ?? filePath)
|
|
1473
|
+
};
|
|
694
1474
|
}
|
|
695
1475
|
throw new HTTPError({
|
|
696
1476
|
statusCode: 404,
|
|
@@ -713,16 +1493,22 @@ function ipxFSStorage(_options = {}) {
|
|
|
713
1493
|
}
|
|
714
1494
|
};
|
|
715
1495
|
}
|
|
716
|
-
const isWindows = process
|
|
1496
|
+
const isWindows = globalThis.process?.platform === "win32";
|
|
717
1497
|
function isValidPath(fp) {
|
|
718
1498
|
if (isWindows) fp = fp.slice(parse(fp).root.length);
|
|
719
1499
|
if (/["*:<>?|]/.test(fp)) return false;
|
|
720
1500
|
return true;
|
|
721
1501
|
}
|
|
1502
|
+
function isInsideDir(filePath, dir) {
|
|
1503
|
+
if (isWindows) {
|
|
1504
|
+
filePath = filePath.toLowerCase();
|
|
1505
|
+
dir = dir.toLowerCase();
|
|
1506
|
+
}
|
|
1507
|
+
const rel = relative(dir, filePath);
|
|
1508
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
1509
|
+
}
|
|
722
1510
|
function resolveDirs(dirs) {
|
|
723
1511
|
if (!dirs || !Array.isArray(dirs)) return [resolve(dirs || getEnv("IPX_FS_DIR") || ".")];
|
|
724
|
-
return dirs.map((dirs
|
|
1512
|
+
return dirs.map((dirs) => resolve(dirs));
|
|
725
1513
|
}
|
|
726
|
-
|
|
727
|
-
//#endregion
|
|
728
|
-
export { serveIPX as a, createIPXNodeHandler as i, ipxHttpStorage as n, createIPX as o, createIPXFetchHandler as r, ipxFSStorage as t };
|
|
1514
|
+
export { createIPX, createIPXFetchHandler, createIPXNodeHandler, ipxFSStorage, ipxHttpStorage, parseIPXURL, serveIPX };
|