@socialtip/asset-proxy-url-parser 0.5.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto.js.map +1 -1
- package/dist/error.d.ts +4 -1
- package/dist/error.js +4 -1
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/info-parse.js.map +1 -1
- package/dist/parse.d.ts +2 -1
- package/dist/parse.js +14 -3
- package/dist/parse.js.map +1 -1
- package/dist/signature.js.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -39
- package/src/crypto.ts +0 -53
- package/src/error.ts +0 -23
- package/src/index.ts +0 -36
- package/src/info-parse.ts +0 -275
- package/src/parse.ts +0 -1426
- package/src/signature.ts +0 -59
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -17
package/src/parse.ts
DELETED
|
@@ -1,1426 +0,0 @@
|
|
|
1
|
-
import { z } from "zod/v4";
|
|
2
|
-
import { decryptSourceUrl } from "./crypto.js";
|
|
3
|
-
import { HTTPError } from "./error.js";
|
|
4
|
-
|
|
5
|
-
const resizingType = z.enum(["fit", "fill", "fill-down", "force", "auto"]);
|
|
6
|
-
export type ResizingType = z.output<typeof resizingType>;
|
|
7
|
-
|
|
8
|
-
const cpuAlgorithms = [
|
|
9
|
-
"nearest",
|
|
10
|
-
"linear",
|
|
11
|
-
"cubic",
|
|
12
|
-
"lanczos2",
|
|
13
|
-
"lanczos3",
|
|
14
|
-
] as const;
|
|
15
|
-
// TODO: support "cuvid" as a third GPU scaler option (uses decoder-level resize via -resize flag)
|
|
16
|
-
const gpuScalers = ["scale_cuda", "scale_npp"] as const;
|
|
17
|
-
type CpuAlgorithm = (typeof cpuAlgorithms)[number];
|
|
18
|
-
type GpuScaler = (typeof gpuScalers)[number];
|
|
19
|
-
|
|
20
|
-
export type ResizingAlgorithm =
|
|
21
|
-
| { mode: "cpu"; algorithm: CpuAlgorithm }
|
|
22
|
-
| { mode: "gpu"; scaler: GpuScaler; algorithm?: CpuAlgorithm };
|
|
23
|
-
|
|
24
|
-
const cpuAlgorithmSet = new Set<string>(cpuAlgorithms);
|
|
25
|
-
const gpuScalerSet = new Set<string>(gpuScalers);
|
|
26
|
-
|
|
27
|
-
const resizingAlgorithmSchema = z.union([
|
|
28
|
-
z.object({
|
|
29
|
-
mode: z.literal("cpu"),
|
|
30
|
-
algorithm: z.enum(cpuAlgorithms),
|
|
31
|
-
}),
|
|
32
|
-
z.object({
|
|
33
|
-
mode: z.literal("gpu"),
|
|
34
|
-
scaler: z.enum(gpuScalers),
|
|
35
|
-
algorithm: z.enum(cpuAlgorithms).optional(),
|
|
36
|
-
}),
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
const zResizingAlgorithm = z.string().transform((v): ResizingAlgorithm => {
|
|
40
|
-
if (v === "gpu") {
|
|
41
|
-
return { mode: "gpu", scaler: "scale_cuda" };
|
|
42
|
-
}
|
|
43
|
-
if (v.startsWith("gpu:")) {
|
|
44
|
-
const parts = v.slice(4).split(":");
|
|
45
|
-
const scaler = parts[0] || "scale_cuda";
|
|
46
|
-
if (!gpuScalerSet.has(scaler)) {
|
|
47
|
-
throw new HTTPError(
|
|
48
|
-
`Invalid GPU scaler '${scaler}': expected one of ${gpuScalers.join(", ")}`,
|
|
49
|
-
{ code: "BAD_REQUEST" },
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
const algo = parts[1];
|
|
53
|
-
if (algo !== undefined) {
|
|
54
|
-
if (!cpuAlgorithmSet.has(algo)) {
|
|
55
|
-
throw new HTTPError(
|
|
56
|
-
`Invalid interpolation algorithm '${algo}': expected one of ${cpuAlgorithms.join(", ")}`,
|
|
57
|
-
{ code: "BAD_REQUEST" },
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
if (scaler !== "scale_npp") {
|
|
61
|
-
throw new HTTPError(
|
|
62
|
-
"Interpolation algorithm is only supported with scale_npp",
|
|
63
|
-
{ code: "BAD_REQUEST" },
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
mode: "gpu",
|
|
68
|
-
scaler: scaler as GpuScaler,
|
|
69
|
-
algorithm: algo as CpuAlgorithm,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return { mode: "gpu", scaler: scaler as GpuScaler };
|
|
73
|
-
}
|
|
74
|
-
if (!cpuAlgorithmSet.has(v)) {
|
|
75
|
-
throw new HTTPError(
|
|
76
|
-
`Invalid resizing algorithm '${v}': expected one of ${cpuAlgorithms.join(", ")} or gpu:<scaler>[:<algorithm>]`,
|
|
77
|
-
{ code: "BAD_REQUEST" },
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return { mode: "cpu", algorithm: v as CpuAlgorithm };
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const videoFormat = z.enum(["mp4", "webm"]);
|
|
84
|
-
const imageFormat = z.enum(["jpg", "png", "webp", "avif", "gif"]);
|
|
85
|
-
const outputFormat = z.union([videoFormat, imageFormat]);
|
|
86
|
-
export type VideoFormat = z.output<typeof videoFormat>;
|
|
87
|
-
export type ImageFormat = z.output<typeof imageFormat>;
|
|
88
|
-
export type OutputFormat = z.output<typeof outputFormat>;
|
|
89
|
-
|
|
90
|
-
export type MediaType = "video" | "image";
|
|
91
|
-
|
|
92
|
-
const compassGravity = z.enum([
|
|
93
|
-
"no",
|
|
94
|
-
"so",
|
|
95
|
-
"ea",
|
|
96
|
-
"we",
|
|
97
|
-
"noea",
|
|
98
|
-
"nowe",
|
|
99
|
-
"soea",
|
|
100
|
-
"sowe",
|
|
101
|
-
"ce",
|
|
102
|
-
]);
|
|
103
|
-
export type CompassGravity = z.output<typeof compassGravity>;
|
|
104
|
-
|
|
105
|
-
const focusPointGravity = z.object({
|
|
106
|
-
type: z.literal("fp"),
|
|
107
|
-
x: z.number(),
|
|
108
|
-
y: z.number(),
|
|
109
|
-
});
|
|
110
|
-
export type FocusPointGravity = z.output<typeof focusPointGravity>;
|
|
111
|
-
|
|
112
|
-
const gravitySchema = z.union([compassGravity, focusPointGravity]);
|
|
113
|
-
export type Gravity = z.output<typeof gravitySchema>;
|
|
114
|
-
|
|
115
|
-
const zGravity = z.string().transform((v): Gravity => {
|
|
116
|
-
if (v.startsWith("fp:")) {
|
|
117
|
-
const [, xStr, yStr] = v.split(":");
|
|
118
|
-
const x = parseFloat(xStr);
|
|
119
|
-
const y = parseFloat(yStr);
|
|
120
|
-
if (
|
|
121
|
-
Number.isNaN(x) ||
|
|
122
|
-
Number.isNaN(y) ||
|
|
123
|
-
x < 0 ||
|
|
124
|
-
x > 1 ||
|
|
125
|
-
y < 0 ||
|
|
126
|
-
y > 1
|
|
127
|
-
) {
|
|
128
|
-
throw new HTTPError(
|
|
129
|
-
"Focus point gravity requires x and y values between 0 and 1: gravity:fp:<x>:<y>",
|
|
130
|
-
{ code: "BAD_REQUEST" },
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return { type: "fp", x, y };
|
|
134
|
-
}
|
|
135
|
-
if (v.startsWith("sm") || v.startsWith("obj")) {
|
|
136
|
-
throw new HTTPError(
|
|
137
|
-
`Gravity type '${v.split(":")[0]}' is not implemented`,
|
|
138
|
-
{
|
|
139
|
-
code: "NOT_IMPLEMENTED",
|
|
140
|
-
},
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return compassGravity.parse(v);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const rgb = z.object({ r: z.number(), g: z.number(), b: z.number() });
|
|
147
|
-
const sides = z.object({
|
|
148
|
-
top: z.number(),
|
|
149
|
-
right: z.number(),
|
|
150
|
-
bottom: z.number(),
|
|
151
|
-
left: z.number(),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const resizeOptions = z.object({
|
|
155
|
-
/** Resize mode: fit, fill, fill-down, force, or auto. */
|
|
156
|
-
type: resizingType,
|
|
157
|
-
width: z.number(),
|
|
158
|
-
height: z.number(),
|
|
159
|
-
});
|
|
160
|
-
export type ResizeOptions = z.output<typeof resizeOptions>;
|
|
161
|
-
|
|
162
|
-
const zBool = z
|
|
163
|
-
.string()
|
|
164
|
-
.transform((v) => v === "1" || v === "t" || v === "true");
|
|
165
|
-
|
|
166
|
-
const zPositiveFloat = z.coerce.number().positive();
|
|
167
|
-
|
|
168
|
-
const zBackground = z.string().transform((v) => {
|
|
169
|
-
const parts = v.split(":");
|
|
170
|
-
if (parts.length === 1) {
|
|
171
|
-
const hex = parts[0].replace(/^#/, "");
|
|
172
|
-
if (hex.length === 3) {
|
|
173
|
-
return {
|
|
174
|
-
r: parseInt(hex[0] + hex[0], 16),
|
|
175
|
-
g: parseInt(hex[1] + hex[1], 16),
|
|
176
|
-
b: parseInt(hex[2] + hex[2], 16),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
r: parseInt(hex.slice(0, 2), 16),
|
|
181
|
-
g: parseInt(hex.slice(2, 4), 16),
|
|
182
|
-
b: parseInt(hex.slice(4, 6), 16),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
return {
|
|
186
|
-
r: parseInt(parts[0], 10) || 0,
|
|
187
|
-
g: parseInt(parts[1], 10) || 0,
|
|
188
|
-
b: parseInt(parts[2], 10) || 0,
|
|
189
|
-
};
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
/** Schema for options that are recognised but not yet implemented. */
|
|
193
|
-
const notImplemented = (name: string) =>
|
|
194
|
-
z.string().transform(() => {
|
|
195
|
-
throw new HTTPError(`Option '${name}' is not implemented`, {
|
|
196
|
-
code: "NOT_IMPLEMENTED",
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const VIDEO_FORMATS = new Set<string>(["mp4", "webm"]);
|
|
201
|
-
const IMAGE_FORMATS = new Set<string>(["jpg", "png", "webp", "avif", "gif"]);
|
|
202
|
-
const ALL_FORMATS = new Set<string>([...VIDEO_FORMATS, ...IMAGE_FORMATS]);
|
|
203
|
-
const IMAGE_EXTENSIONS = /\.(jpe?g|png|webp|avif|gif|svg|bmp|tiff?)$/i;
|
|
204
|
-
|
|
205
|
-
export const SHORTHANDS: Record<string, string> = {
|
|
206
|
-
rs: "resize",
|
|
207
|
-
s: "size",
|
|
208
|
-
t: "resizing_type",
|
|
209
|
-
w: "width",
|
|
210
|
-
h: "height",
|
|
211
|
-
mw: "min_width",
|
|
212
|
-
mh: "min_height",
|
|
213
|
-
z: "zoom",
|
|
214
|
-
el: "enlarge",
|
|
215
|
-
ex: "extend",
|
|
216
|
-
exar: "extend_aspect_ratio",
|
|
217
|
-
c: "crop",
|
|
218
|
-
g: "gravity",
|
|
219
|
-
q: "quality",
|
|
220
|
-
bl: "blur",
|
|
221
|
-
sh: "sharpen",
|
|
222
|
-
rot: "rotate",
|
|
223
|
-
fl: "flip",
|
|
224
|
-
ar: "auto_rotate",
|
|
225
|
-
bg: "background",
|
|
226
|
-
bga: "background_alpha",
|
|
227
|
-
pd: "padding",
|
|
228
|
-
sm: "strip_metadata",
|
|
229
|
-
kcr: "keep_copyright",
|
|
230
|
-
scp: "strip_color_profile",
|
|
231
|
-
f: "format",
|
|
232
|
-
fr: "framerate",
|
|
233
|
-
ct: "cut",
|
|
234
|
-
tr: "trim",
|
|
235
|
-
ra: "resizing_algorithm",
|
|
236
|
-
a: "adjust",
|
|
237
|
-
br: "brightness",
|
|
238
|
-
co: "contrast",
|
|
239
|
-
sa: "saturation",
|
|
240
|
-
mc: "monochrome",
|
|
241
|
-
dt: "duotone",
|
|
242
|
-
px: "pixelate",
|
|
243
|
-
ush: "unsharp_masking",
|
|
244
|
-
bla: "blur_areas",
|
|
245
|
-
bd: "blur_detections",
|
|
246
|
-
dd: "draw_detections",
|
|
247
|
-
clrz: "colorize",
|
|
248
|
-
col: "colorize",
|
|
249
|
-
grd: "gradient",
|
|
250
|
-
gr: "gradient",
|
|
251
|
-
wm: "watermark",
|
|
252
|
-
wmu: "watermark_url",
|
|
253
|
-
wmt: "watermark_text",
|
|
254
|
-
wms: "watermark_size",
|
|
255
|
-
wmr: "watermark_rotate",
|
|
256
|
-
wmsh: "watermark_shadow",
|
|
257
|
-
st: "style",
|
|
258
|
-
eth: "enforce_thumbnail",
|
|
259
|
-
pg: "page",
|
|
260
|
-
pgs: "pages",
|
|
261
|
-
da: "disable_animation",
|
|
262
|
-
vts: "video_thumbnail_second",
|
|
263
|
-
vtk: "video_thumbnail_keyframes",
|
|
264
|
-
vtt: "video_thumbnail_tile",
|
|
265
|
-
vta: "video_thumbnail_animation",
|
|
266
|
-
fq: "format_quality",
|
|
267
|
-
jpgo: "jpeg_options",
|
|
268
|
-
pngo: "png_options",
|
|
269
|
-
wpo: "webp_options",
|
|
270
|
-
avo: "avif_options",
|
|
271
|
-
aq: "autoquality",
|
|
272
|
-
mb: "max_bytes",
|
|
273
|
-
op: "objects_position",
|
|
274
|
-
car: "crop_aspect_ratio",
|
|
275
|
-
skp: "skip_processing",
|
|
276
|
-
cb: "cache_buster",
|
|
277
|
-
exp: "expires",
|
|
278
|
-
fn: "filename",
|
|
279
|
-
att: "return_attachment",
|
|
280
|
-
pr: "preset",
|
|
281
|
-
fiu: "fallback_image_url",
|
|
282
|
-
hs: "hashsum",
|
|
283
|
-
mu: "mute",
|
|
284
|
-
msr: "max_src_resolution",
|
|
285
|
-
msfs: "max_src_file_size",
|
|
286
|
-
maf: "max_animation_frames",
|
|
287
|
-
mafr: "max_animation_frame_resolution",
|
|
288
|
-
mrd: "max_result_dimension",
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const rawOptionsSchema = z
|
|
292
|
-
.object({
|
|
293
|
-
/** Resize with type, width, height. Format: `<type>:<w>:<h>`. */
|
|
294
|
-
resize: z
|
|
295
|
-
.string()
|
|
296
|
-
.transform((v) => {
|
|
297
|
-
const [type = "fit", w, h] = v.split(":");
|
|
298
|
-
return {
|
|
299
|
-
type: resizingType.parse(type),
|
|
300
|
-
width: parseInt(w, 10) || 0,
|
|
301
|
-
height: parseInt(h, 10) || 0,
|
|
302
|
-
};
|
|
303
|
-
})
|
|
304
|
-
.optional(),
|
|
305
|
-
|
|
306
|
-
/** Shorthand for width + height. Format: `<w>:<h>`. */
|
|
307
|
-
size: z
|
|
308
|
-
.string()
|
|
309
|
-
.transform((v) => {
|
|
310
|
-
const [w, h] = v.split(":");
|
|
311
|
-
return { width: parseInt(w, 10) || 0, height: parseInt(h, 10) || 0 };
|
|
312
|
-
})
|
|
313
|
-
.optional(),
|
|
314
|
-
|
|
315
|
-
/** Override resize type without specifying dimensions. */
|
|
316
|
-
resizing_type: resizingType.optional(),
|
|
317
|
-
width: z.coerce.number().int().optional(),
|
|
318
|
-
height: z.coerce.number().int().optional(),
|
|
319
|
-
min_width: z.coerce.number().int().optional(),
|
|
320
|
-
min_height: z.coerce.number().int().optional(),
|
|
321
|
-
|
|
322
|
-
zoom: z
|
|
323
|
-
.string()
|
|
324
|
-
.transform((v) => {
|
|
325
|
-
const parts = v.split(":");
|
|
326
|
-
const x = parseFloat(parts[0]) || 1;
|
|
327
|
-
const y = parts[1] !== undefined ? parseFloat(parts[1]) || 1 : x;
|
|
328
|
-
return { x, y };
|
|
329
|
-
})
|
|
330
|
-
.optional(),
|
|
331
|
-
|
|
332
|
-
/** Device pixel ratio — multiplies dimensions and padding. */
|
|
333
|
-
dpr: z.coerce.number().positive().optional(),
|
|
334
|
-
/** Allow upscaling smaller images. */
|
|
335
|
-
enlarge: zBool.optional(),
|
|
336
|
-
|
|
337
|
-
/** Pad undersized images to fill target dimensions. Format: `<enabled>[:<gravity>]`. */
|
|
338
|
-
extend: z
|
|
339
|
-
.string()
|
|
340
|
-
.transform((v) => {
|
|
341
|
-
const parts = v.split(":");
|
|
342
|
-
const enabled =
|
|
343
|
-
parts[0] === "1" || parts[0] === "t" || parts[0] === "true";
|
|
344
|
-
return {
|
|
345
|
-
enabled,
|
|
346
|
-
gravity: compassGravity.safeParse(parts[1]).data ?? ("ce" as const),
|
|
347
|
-
};
|
|
348
|
-
})
|
|
349
|
-
.optional(),
|
|
350
|
-
|
|
351
|
-
extend_aspect_ratio: z
|
|
352
|
-
.string()
|
|
353
|
-
.transform((v) => {
|
|
354
|
-
const parts = v.split(":");
|
|
355
|
-
const enabled =
|
|
356
|
-
parts[0] === "1" || parts[0] === "t" || parts[0] === "true";
|
|
357
|
-
return {
|
|
358
|
-
enabled,
|
|
359
|
-
gravity: compassGravity.safeParse(parts[1]).data ?? ("ce" as const),
|
|
360
|
-
};
|
|
361
|
-
})
|
|
362
|
-
.optional(),
|
|
363
|
-
|
|
364
|
-
crop: z
|
|
365
|
-
.string()
|
|
366
|
-
.transform((v) => {
|
|
367
|
-
const [w, h, ...rest] = v.split(":");
|
|
368
|
-
const gStr = rest.join(":");
|
|
369
|
-
let cropGravity: Gravity | undefined;
|
|
370
|
-
if (gStr) {
|
|
371
|
-
cropGravity = zGravity.parse(gStr);
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
width: parseFloat(w) || 0,
|
|
375
|
-
height: parseFloat(h) || 0,
|
|
376
|
-
gravity: cropGravity,
|
|
377
|
-
};
|
|
378
|
-
})
|
|
379
|
-
.optional(),
|
|
380
|
-
|
|
381
|
-
gravity: zGravity.optional(),
|
|
382
|
-
quality: z.coerce.number().int().optional(),
|
|
383
|
-
blur: z.coerce.number().optional(),
|
|
384
|
-
sharpen: z.coerce.number().optional(),
|
|
385
|
-
rotate: z.coerce.number().int().optional(),
|
|
386
|
-
flip: z
|
|
387
|
-
.string()
|
|
388
|
-
.transform((v) => {
|
|
389
|
-
const [h, vert] = v.split(":");
|
|
390
|
-
return {
|
|
391
|
-
horizontal: h === "1" || h === "t" || h === "true",
|
|
392
|
-
vertical: vert === "1" || vert === "t" || vert === "true",
|
|
393
|
-
};
|
|
394
|
-
})
|
|
395
|
-
.optional(),
|
|
396
|
-
auto_rotate: zBool.optional(),
|
|
397
|
-
background: zBackground.optional(),
|
|
398
|
-
/** Background opacity (0–1). */
|
|
399
|
-
background_alpha: z.coerce.number().min(0).max(1).optional(),
|
|
400
|
-
|
|
401
|
-
/** Canvas padding. Format: `<top>[:<right>[:<bottom>[:<left>]]]`. */
|
|
402
|
-
padding: z
|
|
403
|
-
.string()
|
|
404
|
-
.transform((v) => {
|
|
405
|
-
const parts = v.split(":").map((p) => parseInt(p, 10) || 0);
|
|
406
|
-
const top = parts[0];
|
|
407
|
-
const right = parts[1] ?? top;
|
|
408
|
-
const bottom = parts[2] ?? top;
|
|
409
|
-
const left = parts[3] ?? right;
|
|
410
|
-
return { top, right, bottom, left };
|
|
411
|
-
})
|
|
412
|
-
.optional(),
|
|
413
|
-
|
|
414
|
-
strip_metadata: zBool.optional(),
|
|
415
|
-
/** Preserve copyright metadata when stripping. */
|
|
416
|
-
keep_copyright: zBool.optional(),
|
|
417
|
-
/** Convert ICC colour profile to sRGB and remove it. */
|
|
418
|
-
strip_color_profile: zBool.optional(),
|
|
419
|
-
|
|
420
|
-
format: z
|
|
421
|
-
.string()
|
|
422
|
-
.transform((v) => (v === "jpeg" ? "jpg" : v))
|
|
423
|
-
.pipe(z.string().refine((v) => v === "best" || ALL_FORMATS.has(v)))
|
|
424
|
-
.optional(),
|
|
425
|
-
|
|
426
|
-
/** Output framerate in fps (video only). */
|
|
427
|
-
framerate: zPositiveFloat.optional(),
|
|
428
|
-
/** Limit video duration in seconds (video only). */
|
|
429
|
-
cut: zPositiveFloat.optional(),
|
|
430
|
-
/** Strip audio from video output. */
|
|
431
|
-
mute: zBool.optional(),
|
|
432
|
-
|
|
433
|
-
/** Remove uniform borders. Format: `<threshold>[:<colour>[:<equal_hor>[:<equal_vert>]]]`. */
|
|
434
|
-
trim: z
|
|
435
|
-
.string()
|
|
436
|
-
.transform((v) => {
|
|
437
|
-
const [threshold, colour, equalHor, equalVert] = v.split(":");
|
|
438
|
-
return {
|
|
439
|
-
threshold: parseFloat(threshold) || 0,
|
|
440
|
-
colour: colour || undefined,
|
|
441
|
-
equalHor: equalHor === "1" || equalHor === "t" || equalHor === "true",
|
|
442
|
-
equalVert:
|
|
443
|
-
equalVert === "1" || equalVert === "t" || equalVert === "true",
|
|
444
|
-
};
|
|
445
|
-
})
|
|
446
|
-
.optional(),
|
|
447
|
-
|
|
448
|
-
/** Pixelate with given block size in pixels. */
|
|
449
|
-
pixelate: z.coerce.number().int().positive().optional(),
|
|
450
|
-
|
|
451
|
-
resizing_algorithm: zResizingAlgorithm.optional(),
|
|
452
|
-
|
|
453
|
-
/** Meta-option: `<brightness>:<contrast>:<saturation>`. */
|
|
454
|
-
adjust: z
|
|
455
|
-
.string()
|
|
456
|
-
.transform((v) => {
|
|
457
|
-
const [b, c, s] = v.split(":");
|
|
458
|
-
return {
|
|
459
|
-
brightness: b ? parseInt(b, 10) : 0,
|
|
460
|
-
contrast: c ? parseFloat(c) : 1,
|
|
461
|
-
saturation: s ? parseFloat(s) : 1,
|
|
462
|
-
};
|
|
463
|
-
})
|
|
464
|
-
.optional(),
|
|
465
|
-
/** Brightness (-255 to 255). */
|
|
466
|
-
brightness: z.coerce.number().int().min(-255).max(255).optional(),
|
|
467
|
-
/** Contrast multiplier (1 = unchanged). */
|
|
468
|
-
contrast: z.coerce.number().positive().optional(),
|
|
469
|
-
/** Saturation multiplier (1 = unchanged). */
|
|
470
|
-
saturation: z.coerce.number().positive().optional(),
|
|
471
|
-
/** Monochrome effect. Format: `<intensity>[:<hex_colour>]`. */
|
|
472
|
-
monochrome: z
|
|
473
|
-
.string()
|
|
474
|
-
.transform((v) => {
|
|
475
|
-
const [intensity, colour] = v.split(":");
|
|
476
|
-
return {
|
|
477
|
-
intensity: parseFloat(intensity) || 0,
|
|
478
|
-
colour: colour || "b3b3b3",
|
|
479
|
-
};
|
|
480
|
-
})
|
|
481
|
-
.optional(),
|
|
482
|
-
/** Duotone effect. Format: `<intensity>[:<shadow_colour>[:<highlight_colour>]]`. */
|
|
483
|
-
duotone: z
|
|
484
|
-
.string()
|
|
485
|
-
.transform((v) => {
|
|
486
|
-
const [intensity, c1, c2] = v.split(":");
|
|
487
|
-
return {
|
|
488
|
-
intensity: parseFloat(intensity) || 0,
|
|
489
|
-
colour1: c1 || "000000",
|
|
490
|
-
colour2: c2 || "ffffff",
|
|
491
|
-
};
|
|
492
|
-
})
|
|
493
|
-
.optional(),
|
|
494
|
-
|
|
495
|
-
/** Unsharp masking. Format: `<mode>:<weight>:<divider>`. */
|
|
496
|
-
unsharp_masking: z
|
|
497
|
-
.string()
|
|
498
|
-
.transform((v) => {
|
|
499
|
-
const [mode, weight, divider] = v.split(":");
|
|
500
|
-
return {
|
|
501
|
-
mode: mode || "auto",
|
|
502
|
-
weight: weight ? parseFloat(weight) : 1,
|
|
503
|
-
divider: divider ? parseFloat(divider) : 24,
|
|
504
|
-
};
|
|
505
|
-
})
|
|
506
|
-
.optional(),
|
|
507
|
-
|
|
508
|
-
blur_areas: notImplemented("blur_areas").optional(),
|
|
509
|
-
blur_detections: notImplemented("blur_detections").optional(),
|
|
510
|
-
draw_detections: notImplemented("draw_detections").optional(),
|
|
511
|
-
|
|
512
|
-
/** Colour overlay. Format: `<opacity>[:<hex_colour>[:<keep_alpha>]]`. */
|
|
513
|
-
colorize: z
|
|
514
|
-
.string()
|
|
515
|
-
.transform((v) => {
|
|
516
|
-
const [opacity, colour, keepAlpha] = v.split(":");
|
|
517
|
-
return {
|
|
518
|
-
opacity: parseFloat(opacity) || 0,
|
|
519
|
-
colour: colour || "000",
|
|
520
|
-
keepAlpha:
|
|
521
|
-
keepAlpha === "1" || keepAlpha === "t" || keepAlpha === "true",
|
|
522
|
-
};
|
|
523
|
-
})
|
|
524
|
-
.optional(),
|
|
525
|
-
|
|
526
|
-
/** Gradient overlay. Format: `<opacity>[:<colour>[:<direction>[:<start>[:<stop>]]]]`. */
|
|
527
|
-
gradient: z
|
|
528
|
-
.string()
|
|
529
|
-
.transform((v) => {
|
|
530
|
-
const [opacity, colour, direction, start, stop] = v.split(":");
|
|
531
|
-
return {
|
|
532
|
-
opacity: parseFloat(opacity) || 0,
|
|
533
|
-
colour: colour || "000",
|
|
534
|
-
direction: direction || "down",
|
|
535
|
-
start: start ? parseFloat(start) : 0,
|
|
536
|
-
stop: stop ? parseFloat(stop) : 1,
|
|
537
|
-
};
|
|
538
|
-
})
|
|
539
|
-
.optional(),
|
|
540
|
-
|
|
541
|
-
/** Watermark overlay. Format: `<opacity>[:<position>[:<x_offset>[:<y_offset>[:<scale>]]]]`. */
|
|
542
|
-
watermark: notImplemented("watermark").optional(),
|
|
543
|
-
/** Custom watermark image URL (base64-encoded). */
|
|
544
|
-
watermark_url: notImplemented("watermark_url").optional(),
|
|
545
|
-
/** Watermark text (base64-encoded, supports Pango markup). */
|
|
546
|
-
watermark_text: notImplemented("watermark_text").optional(),
|
|
547
|
-
/** Watermark dimensions. Format: `<width>:<height>`. */
|
|
548
|
-
watermark_size: notImplemented("watermark_size").optional(),
|
|
549
|
-
/** Watermark rotation in degrees. */
|
|
550
|
-
watermark_rotate: notImplemented("watermark_rotate").optional(),
|
|
551
|
-
/** Watermark shadow blur sigma. */
|
|
552
|
-
watermark_shadow: notImplemented("watermark_shadow").optional(),
|
|
553
|
-
/** Text style (Pango markup). */
|
|
554
|
-
style: notImplemented("style").optional(),
|
|
555
|
-
|
|
556
|
-
/** Set output DPI metadata. */
|
|
557
|
-
dpi: z.coerce.number().positive().optional(),
|
|
558
|
-
/** Prefer embedded thumbnail over full image (HEIC/AVIF). */
|
|
559
|
-
enforce_thumbnail: zBool.optional(),
|
|
560
|
-
|
|
561
|
-
/** Per-format quality. Format: `<fmt1>:<q1>:<fmt2>:<q2>:...`. */
|
|
562
|
-
format_quality: z
|
|
563
|
-
.string()
|
|
564
|
-
.transform((v) => {
|
|
565
|
-
const parts = v.split(":");
|
|
566
|
-
const result: Record<string, number> = {};
|
|
567
|
-
for (let i = 0; i < parts.length - 1; i += 2) {
|
|
568
|
-
const fmt = parts[i] === "jpeg" ? "jpg" : parts[i];
|
|
569
|
-
result[fmt] = parseInt(parts[i + 1], 10);
|
|
570
|
-
}
|
|
571
|
-
return result;
|
|
572
|
-
})
|
|
573
|
-
.optional(),
|
|
574
|
-
/** Autoquality. Format: `<method>:<target>:<min>:<max>:<allowed_error>`. Methods: dssim, size. */
|
|
575
|
-
autoquality: z
|
|
576
|
-
.string()
|
|
577
|
-
.transform((v) => {
|
|
578
|
-
const [method, target, min, max, err] = v.split(":");
|
|
579
|
-
const m = method || "dssim";
|
|
580
|
-
if (m !== "dssim" && m !== "size") {
|
|
581
|
-
throw new HTTPError(
|
|
582
|
-
`Autoquality method '${m}' is not implemented — supported: dssim, size`,
|
|
583
|
-
{ code: "NOT_IMPLEMENTED" },
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
method: m as "dssim" | "size",
|
|
588
|
-
target: target ? parseFloat(target) : m === "size" ? 0 : 0.02,
|
|
589
|
-
min: min ? parseInt(min, 10) : 70,
|
|
590
|
-
max: max ? parseInt(max, 10) : 80,
|
|
591
|
-
allowedError: err ? parseFloat(err) : 0.001,
|
|
592
|
-
};
|
|
593
|
-
})
|
|
594
|
-
.optional(),
|
|
595
|
-
/** Max output size in bytes — degrades quality until under limit. */
|
|
596
|
-
max_bytes: z.coerce.number().int().positive().optional(),
|
|
597
|
-
|
|
598
|
-
/** JPEG options. Format: `<progressive>:<no_subsample>:<trellis_quant>:<overshoot_deringing>:<optimize_scans>:<quant_table>`. */
|
|
599
|
-
jpeg_options: z
|
|
600
|
-
.string()
|
|
601
|
-
.transform((v) => {
|
|
602
|
-
const [
|
|
603
|
-
progressive,
|
|
604
|
-
noSubsample,
|
|
605
|
-
trellisQuant,
|
|
606
|
-
overshootDeringing,
|
|
607
|
-
optimizeScans,
|
|
608
|
-
quantTable,
|
|
609
|
-
] = v.split(":");
|
|
610
|
-
return {
|
|
611
|
-
progressive:
|
|
612
|
-
progressive === "1" ||
|
|
613
|
-
progressive === "t" ||
|
|
614
|
-
progressive === "true",
|
|
615
|
-
noSubsample:
|
|
616
|
-
noSubsample === "1" ||
|
|
617
|
-
noSubsample === "t" ||
|
|
618
|
-
noSubsample === "true",
|
|
619
|
-
trellisQuant:
|
|
620
|
-
trellisQuant === "1" ||
|
|
621
|
-
trellisQuant === "t" ||
|
|
622
|
-
trellisQuant === "true",
|
|
623
|
-
overshootDeringing:
|
|
624
|
-
overshootDeringing === "1" ||
|
|
625
|
-
overshootDeringing === "t" ||
|
|
626
|
-
overshootDeringing === "true",
|
|
627
|
-
optimizeScans:
|
|
628
|
-
optimizeScans === "1" ||
|
|
629
|
-
optimizeScans === "t" ||
|
|
630
|
-
optimizeScans === "true",
|
|
631
|
-
quantTable: quantTable ? parseInt(quantTable, 10) : undefined,
|
|
632
|
-
};
|
|
633
|
-
})
|
|
634
|
-
.optional(),
|
|
635
|
-
/** PNG options. Format: `<interlaced>:<quantize>:<quantization_colours>`. */
|
|
636
|
-
png_options: z
|
|
637
|
-
.string()
|
|
638
|
-
.transform((v) => {
|
|
639
|
-
const [interlaced, quantize, colours] = v.split(":");
|
|
640
|
-
return {
|
|
641
|
-
interlaced:
|
|
642
|
-
interlaced === "1" || interlaced === "t" || interlaced === "true",
|
|
643
|
-
quantize: quantize === "1" || quantize === "t" || quantize === "true",
|
|
644
|
-
quantizationColours: colours ? parseInt(colours, 10) : undefined,
|
|
645
|
-
};
|
|
646
|
-
})
|
|
647
|
-
.optional(),
|
|
648
|
-
/** WebP options. Format: `<compression>:<smart_subsample>:<preset>`. */
|
|
649
|
-
webp_options: z
|
|
650
|
-
.string()
|
|
651
|
-
.transform((v) => {
|
|
652
|
-
const [compression, smartSubsample, preset] = v.split(":");
|
|
653
|
-
return {
|
|
654
|
-
compression: compression ? parseInt(compression, 10) : undefined,
|
|
655
|
-
smartSubsample:
|
|
656
|
-
smartSubsample === "1" ||
|
|
657
|
-
smartSubsample === "t" ||
|
|
658
|
-
smartSubsample === "true",
|
|
659
|
-
preset: preset || undefined,
|
|
660
|
-
};
|
|
661
|
-
})
|
|
662
|
-
.optional(),
|
|
663
|
-
/** AVIF options. Format: `<subsample>`. */
|
|
664
|
-
avif_options: z
|
|
665
|
-
.string()
|
|
666
|
-
.transform((v) => {
|
|
667
|
-
return { subsample: v || undefined };
|
|
668
|
-
})
|
|
669
|
-
.optional(),
|
|
670
|
-
|
|
671
|
-
page: notImplemented("page").optional(),
|
|
672
|
-
pages: notImplemented("pages").optional(),
|
|
673
|
-
disable_animation: notImplemented("disable_animation").optional(),
|
|
674
|
-
|
|
675
|
-
/** Extract video frame at given second. */
|
|
676
|
-
video_thumbnail_second: z.coerce.number().optional(),
|
|
677
|
-
/** Use only keyframes for video thumbnails. */
|
|
678
|
-
video_thumbnail_keyframes: zBool.optional(),
|
|
679
|
-
video_thumbnail_tile: notImplemented("video_thumbnail_tile").optional(),
|
|
680
|
-
/** Video animation. Format: `<step>:<delay>:<frames>:<frame_width>:<frame_height>:<extend_frame>:<trim>:<fill>:<focus_x>:<focus_y>`. */
|
|
681
|
-
video_thumbnail_animation: z
|
|
682
|
-
.string()
|
|
683
|
-
.transform((v) => {
|
|
684
|
-
const [
|
|
685
|
-
step,
|
|
686
|
-
delay,
|
|
687
|
-
frames,
|
|
688
|
-
frameWidth,
|
|
689
|
-
frameHeight,
|
|
690
|
-
extendFrame,
|
|
691
|
-
trim,
|
|
692
|
-
fill,
|
|
693
|
-
focusX,
|
|
694
|
-
focusY,
|
|
695
|
-
] = v.split(":");
|
|
696
|
-
return {
|
|
697
|
-
step: step ? parseFloat(step) : 0,
|
|
698
|
-
delay: delay ? parseInt(delay, 10) : 100,
|
|
699
|
-
frames: frames ? parseInt(frames, 10) : 0,
|
|
700
|
-
frameWidth: frameWidth ? parseInt(frameWidth, 10) : 0,
|
|
701
|
-
frameHeight: frameHeight ? parseInt(frameHeight, 10) : 0,
|
|
702
|
-
extendFrame: extendFrame === "1",
|
|
703
|
-
trim: trim === "1",
|
|
704
|
-
fill: fill === "1",
|
|
705
|
-
focusX: focusX ? parseFloat(focusX) : 0.5,
|
|
706
|
-
focusY: focusY ? parseFloat(focusY) : 0.5,
|
|
707
|
-
};
|
|
708
|
-
})
|
|
709
|
-
.optional(),
|
|
710
|
-
|
|
711
|
-
/** Skip processing for listed extensions. Format: `<ext1>:<ext2>:...`. */
|
|
712
|
-
skip_processing: z
|
|
713
|
-
.string()
|
|
714
|
-
.transform((v) => {
|
|
715
|
-
if (!v) return [];
|
|
716
|
-
return v.split(":").map((e) => (e === "jpeg" ? "jpg" : e));
|
|
717
|
-
})
|
|
718
|
-
.optional(),
|
|
719
|
-
/** Return source without any processing. */
|
|
720
|
-
raw: zBool.optional(),
|
|
721
|
-
/** Ignored value used to differentiate CDN cache keys. */
|
|
722
|
-
cache_buster: z.string().optional(),
|
|
723
|
-
/** Unix timestamp after which the URL returns 404. */
|
|
724
|
-
expires: z.coerce.number().int().optional(),
|
|
725
|
-
/** Override the download filename in Content-Disposition. */
|
|
726
|
-
filename: z.string().optional(),
|
|
727
|
-
/** When true, set Content-Disposition: attachment. */
|
|
728
|
-
return_attachment: zBool.optional(),
|
|
729
|
-
preset: notImplemented("preset").optional(),
|
|
730
|
-
/** Fallback image URL (base64url-encoded) served when the source fails to load. */
|
|
731
|
-
fallback_image_url: z.string().optional(),
|
|
732
|
-
/** Expected hex-encoded checksum of the source image. Format: `<type>:<hex_digest>`. */
|
|
733
|
-
hashsum: z
|
|
734
|
-
.string()
|
|
735
|
-
.transform((v) => {
|
|
736
|
-
const idx = v.indexOf(":");
|
|
737
|
-
if (idx === -1) {
|
|
738
|
-
throw new HTTPError("hashsum requires format <type>:<hex_digest>", {
|
|
739
|
-
code: "BAD_REQUEST",
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
const type = v.slice(0, idx);
|
|
743
|
-
const hash = v.slice(idx + 1);
|
|
744
|
-
return { type, hash };
|
|
745
|
-
})
|
|
746
|
-
.optional(),
|
|
747
|
-
|
|
748
|
-
/** Max source resolution in megapixels. */
|
|
749
|
-
max_src_resolution: z.coerce.number().positive().optional(),
|
|
750
|
-
/** Max source file size in bytes. */
|
|
751
|
-
max_src_file_size: z.coerce.number().int().positive().optional(),
|
|
752
|
-
/** Max animation frames. */
|
|
753
|
-
max_animation_frames: z.coerce.number().int().positive().optional(),
|
|
754
|
-
/** Max animation frame resolution in megapixels. */
|
|
755
|
-
max_animation_frame_resolution: z.coerce.number().positive().optional(),
|
|
756
|
-
/** Max result width or height in pixels. */
|
|
757
|
-
max_result_dimension: z.coerce.number().int().positive().optional(),
|
|
758
|
-
|
|
759
|
-
objects_position: notImplemented("objects_position").optional(),
|
|
760
|
-
crop_aspect_ratio: z
|
|
761
|
-
.string()
|
|
762
|
-
.transform((v) => {
|
|
763
|
-
const [w, h] = v.split(":");
|
|
764
|
-
const width = parseFloat(w);
|
|
765
|
-
const height = parseFloat(h);
|
|
766
|
-
if (!width || !height || width <= 0 || height <= 0) {
|
|
767
|
-
throw new HTTPError(
|
|
768
|
-
"crop_aspect_ratio requires two positive numbers: car:<width>:<height>",
|
|
769
|
-
{ code: "BAD_REQUEST" },
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
return width / height;
|
|
773
|
-
})
|
|
774
|
-
.optional(),
|
|
775
|
-
})
|
|
776
|
-
.passthrough();
|
|
777
|
-
|
|
778
|
-
const optionsSchema = rawOptionsSchema.transform((data) => {
|
|
779
|
-
let resizeType = data.resize?.type ?? data.resizing_type ?? ("fit" as const);
|
|
780
|
-
let resize = data.resize;
|
|
781
|
-
const w = data.size?.width ?? data.width;
|
|
782
|
-
const h = data.size?.height ?? data.height;
|
|
783
|
-
|
|
784
|
-
// Apply standalone resizing_type
|
|
785
|
-
if (data.resizing_type && resize) {
|
|
786
|
-
resize = { ...resize, type: data.resizing_type };
|
|
787
|
-
resizeType = data.resizing_type;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (!resize && (w || h)) {
|
|
791
|
-
resize = { type: resizeType, width: w ?? 0, height: h ?? 0 };
|
|
792
|
-
} else if (resize) {
|
|
793
|
-
if (w) resize.width = w;
|
|
794
|
-
if (h) resize.height = h;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Apply zoom multiplier to dimensions
|
|
798
|
-
if (data.zoom && resize) {
|
|
799
|
-
resize = {
|
|
800
|
-
...resize,
|
|
801
|
-
width: resize.width ? Math.round(resize.width * data.zoom.x) : 0,
|
|
802
|
-
height: resize.height ? Math.round(resize.height * data.zoom.y) : 0,
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Apply dpr multiplier to dimensions and padding
|
|
807
|
-
let padding = data.padding;
|
|
808
|
-
if (data.dpr && data.dpr !== 1) {
|
|
809
|
-
const d = data.dpr;
|
|
810
|
-
if (resize) {
|
|
811
|
-
resize = {
|
|
812
|
-
...resize,
|
|
813
|
-
width: resize.width ? Math.round(resize.width * d) : 0,
|
|
814
|
-
height: resize.height ? Math.round(resize.height * d) : 0,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
if (padding) {
|
|
818
|
-
padding = {
|
|
819
|
-
top: Math.round(padding.top * d),
|
|
820
|
-
right: Math.round(padding.right * d),
|
|
821
|
-
bottom: Math.round(padding.bottom * d),
|
|
822
|
-
left: Math.round(padding.left * d),
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return {
|
|
828
|
-
resize,
|
|
829
|
-
resizingAlgorithm: data.resizing_algorithm,
|
|
830
|
-
minWidth: data.min_width,
|
|
831
|
-
minHeight: data.min_height,
|
|
832
|
-
extend: data.extend,
|
|
833
|
-
extendAspectRatio: data.extend_aspect_ratio,
|
|
834
|
-
framerate: data.framerate,
|
|
835
|
-
cut: data.cut,
|
|
836
|
-
mute: data.mute,
|
|
837
|
-
trim: data.trim,
|
|
838
|
-
brightness: data.brightness ?? data.adjust?.brightness ?? 0,
|
|
839
|
-
contrast: data.contrast ?? data.adjust?.contrast ?? 1,
|
|
840
|
-
saturation: data.saturation ?? data.adjust?.saturation ?? 1,
|
|
841
|
-
monochrome: data.monochrome,
|
|
842
|
-
duotone: data.duotone,
|
|
843
|
-
quality: data.quality,
|
|
844
|
-
formatQuality: data.format_quality,
|
|
845
|
-
autoquality: data.autoquality,
|
|
846
|
-
maxBytes: data.max_bytes,
|
|
847
|
-
jpegOptions: data.jpeg_options,
|
|
848
|
-
pngOptions: data.png_options,
|
|
849
|
-
webpOptions: data.webp_options,
|
|
850
|
-
avifOptions: data.avif_options,
|
|
851
|
-
blur: data.blur,
|
|
852
|
-
sharpen: data.sharpen,
|
|
853
|
-
pixelate: data.pixelate,
|
|
854
|
-
unsharpMasking: data.unsharp_masking,
|
|
855
|
-
colorize: data.colorize,
|
|
856
|
-
gradient: data.gradient,
|
|
857
|
-
rotate: data.rotate,
|
|
858
|
-
flip: data.flip,
|
|
859
|
-
autoRotate: data.auto_rotate,
|
|
860
|
-
background: data.background,
|
|
861
|
-
backgroundAlpha: data.background_alpha,
|
|
862
|
-
padding,
|
|
863
|
-
stripMetadata: data.strip_metadata,
|
|
864
|
-
dpi: data.dpi,
|
|
865
|
-
enforceThumbnail: data.enforce_thumbnail,
|
|
866
|
-
videoThumbnailSecond: data.video_thumbnail_second,
|
|
867
|
-
videoThumbnailKeyframes: data.video_thumbnail_keyframes,
|
|
868
|
-
videoThumbnailAnimation: data.video_thumbnail_animation,
|
|
869
|
-
keepCopyright: data.keep_copyright,
|
|
870
|
-
stripColorProfile: data.strip_color_profile,
|
|
871
|
-
crop: data.crop,
|
|
872
|
-
cropAspectRatio: data.crop_aspect_ratio,
|
|
873
|
-
gravity: data.gravity,
|
|
874
|
-
skipProcessing: data.skip_processing,
|
|
875
|
-
raw: data.raw,
|
|
876
|
-
cacheBuster: data.cache_buster,
|
|
877
|
-
expires: data.expires,
|
|
878
|
-
filename: data.filename,
|
|
879
|
-
returnAttachment: data.return_attachment,
|
|
880
|
-
fallbackImageUrl: data.fallback_image_url,
|
|
881
|
-
hashsum: data.hashsum,
|
|
882
|
-
maxSrcResolution: data.max_src_resolution,
|
|
883
|
-
maxSrcFileSize: data.max_src_file_size,
|
|
884
|
-
maxAnimationFrames: data.max_animation_frames,
|
|
885
|
-
maxAnimationFrameResolution: data.max_animation_frame_resolution,
|
|
886
|
-
maxResultDimension: data.max_result_dimension,
|
|
887
|
-
enlarge: data.enlarge,
|
|
888
|
-
bestFormat: data.format === "best" ? true : undefined,
|
|
889
|
-
formatOverride:
|
|
890
|
-
data.format && data.format !== "best"
|
|
891
|
-
? (data.format as OutputFormat)
|
|
892
|
-
: undefined,
|
|
893
|
-
};
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
/** All processing options for an asset-proxy URL. Defined as an explicit interface so JSDoc is preserved in declaration output. */
|
|
897
|
-
export interface ParsedUrlInput {
|
|
898
|
-
/** Resize dimensions and mode (fit, fill, fill-down, force, auto). */
|
|
899
|
-
resize?: ResizeOptions;
|
|
900
|
-
/** Scaling algorithm — CPU interpolation (e.g. lanczos3) or GPU scaler (scale_cuda, scale_npp). */
|
|
901
|
-
resizingAlgorithm?: ResizingAlgorithm;
|
|
902
|
-
/** The source image/video URL to process. */
|
|
903
|
-
sourceUrl: string;
|
|
904
|
-
/** Output format: jpg, png, webp, avif, gif, mp4, webm. */
|
|
905
|
-
outputFormat: OutputFormat;
|
|
906
|
-
/** Minimum output width — upscales if the result would be narrower. */
|
|
907
|
-
minWidth?: number;
|
|
908
|
-
/** Minimum output height — upscales if the result would be shorter. */
|
|
909
|
-
minHeight?: number;
|
|
910
|
-
/** Pad undersized images to fill the target resize dimensions. */
|
|
911
|
-
extend?: { enabled: boolean; gravity: CompassGravity };
|
|
912
|
-
/** Extend the image to match the target aspect ratio. */
|
|
913
|
-
extendAspectRatio?: { enabled: boolean; gravity: CompassGravity };
|
|
914
|
-
/** Output framerate in fps (video only). */
|
|
915
|
-
framerate?: number;
|
|
916
|
-
/** Limit output duration in seconds (video only). */
|
|
917
|
-
cut?: number;
|
|
918
|
-
/** Strip audio from video output. */
|
|
919
|
-
mute?: boolean;
|
|
920
|
-
/** Remove uniform borders from an image via cropdetect. */
|
|
921
|
-
trim?: {
|
|
922
|
-
/** Colour similarity tolerance (0–255). */
|
|
923
|
-
threshold: number;
|
|
924
|
-
/** Hex colour to trim. Auto-detected if omitted. */
|
|
925
|
-
colour?: string;
|
|
926
|
-
/** Trim equal amounts from left and right. */
|
|
927
|
-
equalHor: boolean;
|
|
928
|
-
/** Trim equal amounts from top and bottom. */
|
|
929
|
-
equalVert: boolean;
|
|
930
|
-
};
|
|
931
|
-
/** Brightness adjustment (-255 to 255, 0 = no change). */
|
|
932
|
-
brightness: number;
|
|
933
|
-
/** Contrast multiplier (1 = no change). */
|
|
934
|
-
contrast: number;
|
|
935
|
-
/** Saturation multiplier (1 = no change). */
|
|
936
|
-
saturation: number;
|
|
937
|
-
/** Convert to monochrome with optional intensity and base colour. */
|
|
938
|
-
monochrome?: { intensity: number; colour: string };
|
|
939
|
-
/** Apply duotone effect with two colours. */
|
|
940
|
-
duotone?: { intensity: number; colour1: string; colour2: string };
|
|
941
|
-
/** Output quality 1–100 for lossy formats (JPEG, WebP, AVIF). */
|
|
942
|
-
quality?: number;
|
|
943
|
-
/** Per-format quality overrides: { jpg: 80, webp: 90, ... }. */
|
|
944
|
-
formatQuality?: Record<string, number>;
|
|
945
|
-
/** Autoquality: dssim (target DSSIM) or size (target bytes). */
|
|
946
|
-
autoquality?: {
|
|
947
|
-
method: "dssim" | "size";
|
|
948
|
-
target: number;
|
|
949
|
-
min: number;
|
|
950
|
-
max: number;
|
|
951
|
-
allowedError: number;
|
|
952
|
-
};
|
|
953
|
-
/** Max output size in bytes — degrades quality to fit. */
|
|
954
|
-
maxBytes?: number;
|
|
955
|
-
/** JPEG encoder options. */
|
|
956
|
-
jpegOptions?: {
|
|
957
|
-
progressive: boolean;
|
|
958
|
-
noSubsample: boolean;
|
|
959
|
-
trellisQuant: boolean;
|
|
960
|
-
overshootDeringing: boolean;
|
|
961
|
-
optimizeScans: boolean;
|
|
962
|
-
quantTable?: number;
|
|
963
|
-
};
|
|
964
|
-
/** PNG encoder options. */
|
|
965
|
-
pngOptions?: {
|
|
966
|
-
interlaced: boolean;
|
|
967
|
-
quantize: boolean;
|
|
968
|
-
quantizationColours?: number;
|
|
969
|
-
};
|
|
970
|
-
/** WebP encoder options. */
|
|
971
|
-
webpOptions?: {
|
|
972
|
-
compression?: number;
|
|
973
|
-
smartSubsample: boolean;
|
|
974
|
-
preset?: string;
|
|
975
|
-
};
|
|
976
|
-
/** AVIF encoder options. */
|
|
977
|
-
avifOptions?: { subsample?: string };
|
|
978
|
-
/** Gaussian blur sigma. */
|
|
979
|
-
blur?: number;
|
|
980
|
-
/** Sharpening sigma. */
|
|
981
|
-
sharpen?: number;
|
|
982
|
-
/** Pixelate block size in pixels. */
|
|
983
|
-
pixelate?: number;
|
|
984
|
-
/** Unsharp masking: mode (auto/always/none), weight, divider. */
|
|
985
|
-
unsharpMasking?: { mode: string; weight: number; divider: number };
|
|
986
|
-
/** Colour overlay with opacity. */
|
|
987
|
-
colorize?: { opacity: number; colour: string; keepAlpha: boolean };
|
|
988
|
-
/** Gradient overlay from transparent to colour. */
|
|
989
|
-
gradient?: {
|
|
990
|
-
opacity: number;
|
|
991
|
-
colour: string;
|
|
992
|
-
direction: string;
|
|
993
|
-
start: number;
|
|
994
|
-
stop: number;
|
|
995
|
-
};
|
|
996
|
-
/** Rotation angle: 0, 90, 180, or 270 degrees. */
|
|
997
|
-
rotate?: number;
|
|
998
|
-
/** Flip the image horizontally and/or vertically. */
|
|
999
|
-
flip?: { horizontal: boolean; vertical: boolean };
|
|
1000
|
-
/** Rotate based on EXIF orientation data. */
|
|
1001
|
-
autoRotate?: boolean;
|
|
1002
|
-
/** Background colour (RGB) for padding, extend, and alpha flattening. */
|
|
1003
|
-
background?: { r: number; g: number; b: number };
|
|
1004
|
-
/** Background opacity (0–1). */
|
|
1005
|
-
backgroundAlpha?: number;
|
|
1006
|
-
/** Canvas padding in pixels: top, right, bottom, left. */
|
|
1007
|
-
padding?: { top: number; right: number; bottom: number; left: number };
|
|
1008
|
-
/** Remove EXIF and other metadata from the output. */
|
|
1009
|
-
stripMetadata?: boolean;
|
|
1010
|
-
/** Preserve copyright metadata when stripping. */
|
|
1011
|
-
keepCopyright?: boolean;
|
|
1012
|
-
/** Convert ICC colour profile to sRGB and remove it. */
|
|
1013
|
-
stripColorProfile?: boolean;
|
|
1014
|
-
/** Output DPI metadata value. */
|
|
1015
|
-
dpi?: number;
|
|
1016
|
-
/** Prefer embedded thumbnail over full image (HEIC/AVIF). */
|
|
1017
|
-
enforceThumbnail?: boolean;
|
|
1018
|
-
/** Extract video frame at this second. */
|
|
1019
|
-
videoThumbnailSecond?: number;
|
|
1020
|
-
/** Use only keyframes for video thumbnails. */
|
|
1021
|
-
videoThumbnailKeyframes?: boolean;
|
|
1022
|
-
/** Video animation config. */
|
|
1023
|
-
videoThumbnailAnimation?: {
|
|
1024
|
-
/** Interval in seconds between sampled video frames (0 = auto). */
|
|
1025
|
-
step: number;
|
|
1026
|
-
/** Delay between animation frames in milliseconds. */
|
|
1027
|
-
delay: number;
|
|
1028
|
-
/** Maximum number of output frames (0 = unlimited). */
|
|
1029
|
-
frames: number;
|
|
1030
|
-
/** Target frame width in pixels (0 = derive from aspect ratio). */
|
|
1031
|
-
frameWidth: number;
|
|
1032
|
-
/** Target frame height in pixels (0 = derive from aspect ratio). */
|
|
1033
|
-
frameHeight: number;
|
|
1034
|
-
/** Pad frames with black to match exact frameWidth/frameHeight. */
|
|
1035
|
-
extendFrame: boolean;
|
|
1036
|
-
/** Remove unused frames from the animation. */
|
|
1037
|
-
trim: boolean;
|
|
1038
|
-
/** Crop-fill to exact frameWidth/frameHeight instead of fitting. */
|
|
1039
|
-
fill: boolean;
|
|
1040
|
-
/** Horizontal crop anchor for fill mode (0–1, default 0.5). */
|
|
1041
|
-
focusX: number;
|
|
1042
|
-
/** Vertical crop anchor for fill mode (0–1, default 0.5). */
|
|
1043
|
-
focusY: number;
|
|
1044
|
-
};
|
|
1045
|
-
/** Extract a region before resizing (width, height, optional gravity). */
|
|
1046
|
-
crop?: { width: number; height: number; gravity?: Gravity };
|
|
1047
|
-
/** Crop to a target aspect ratio (width/height as a float). */
|
|
1048
|
-
cropAspectRatio?: number;
|
|
1049
|
-
/** Anchor point for crop: compass direction or focus point. */
|
|
1050
|
-
gravity?: Gravity;
|
|
1051
|
-
/** Allow upscaling when the image is smaller than the target. */
|
|
1052
|
-
enlarge?: boolean;
|
|
1053
|
-
/** Automatically select the most efficient output format. */
|
|
1054
|
-
bestFormat?: boolean;
|
|
1055
|
-
/** Skip processing when the source extension matches one of these formats. */
|
|
1056
|
-
skipProcessing?: string[];
|
|
1057
|
-
/** Return the source without any processing. */
|
|
1058
|
-
raw?: boolean;
|
|
1059
|
-
/** Ignored cache-busting value. */
|
|
1060
|
-
cacheBuster?: string;
|
|
1061
|
-
/** Unix timestamp after which the URL returns 404. */
|
|
1062
|
-
expires?: number;
|
|
1063
|
-
/** Override the download filename in Content-Disposition. */
|
|
1064
|
-
filename?: string;
|
|
1065
|
-
/** When true, set Content-Disposition: attachment. */
|
|
1066
|
-
returnAttachment?: boolean;
|
|
1067
|
-
/** Fallback image URL (base64url-encoded) to serve when the source fails. */
|
|
1068
|
-
fallbackImageUrl?: string;
|
|
1069
|
-
/** Expected checksum of the source image. */
|
|
1070
|
-
hashsum?: { type: string; hash: string };
|
|
1071
|
-
/** Max source resolution in megapixels. */
|
|
1072
|
-
maxSrcResolution?: number;
|
|
1073
|
-
/** Max source file size in bytes. */
|
|
1074
|
-
maxSrcFileSize?: number;
|
|
1075
|
-
/** Max animation frames. */
|
|
1076
|
-
maxAnimationFrames?: number;
|
|
1077
|
-
/** Max animation frame resolution in megapixels. */
|
|
1078
|
-
maxAnimationFrameResolution?: number;
|
|
1079
|
-
/** Max result width or height in pixels. */
|
|
1080
|
-
maxResultDimension?: number;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/** Zod schema for runtime validation of parsed asset-proxy URL options. The `ParsedUrlInput` interface is the authoritative type definition; this schema validates against it at compile time via `satisfies`. */
|
|
1084
|
-
export const parsedUrlSchema = z.object({
|
|
1085
|
-
resize: resizeOptions.optional(),
|
|
1086
|
-
resizingAlgorithm: resizingAlgorithmSchema.optional(),
|
|
1087
|
-
sourceUrl: z.string(),
|
|
1088
|
-
outputFormat,
|
|
1089
|
-
minWidth: z.number().optional(),
|
|
1090
|
-
minHeight: z.number().optional(),
|
|
1091
|
-
extend: z
|
|
1092
|
-
.object({ enabled: z.boolean(), gravity: compassGravity })
|
|
1093
|
-
.optional(),
|
|
1094
|
-
extendAspectRatio: z
|
|
1095
|
-
.object({ enabled: z.boolean(), gravity: compassGravity })
|
|
1096
|
-
.optional(),
|
|
1097
|
-
framerate: z.number().optional(),
|
|
1098
|
-
cut: z.number().optional(),
|
|
1099
|
-
mute: z.boolean().optional(),
|
|
1100
|
-
trim: z
|
|
1101
|
-
.object({
|
|
1102
|
-
threshold: z.number(),
|
|
1103
|
-
colour: z.string().optional(),
|
|
1104
|
-
equalHor: z.boolean(),
|
|
1105
|
-
equalVert: z.boolean(),
|
|
1106
|
-
})
|
|
1107
|
-
.optional(),
|
|
1108
|
-
brightness: z.number(),
|
|
1109
|
-
contrast: z.number(),
|
|
1110
|
-
saturation: z.number(),
|
|
1111
|
-
monochrome: z
|
|
1112
|
-
.object({ intensity: z.number(), colour: z.string() })
|
|
1113
|
-
.optional(),
|
|
1114
|
-
duotone: z
|
|
1115
|
-
.object({
|
|
1116
|
-
intensity: z.number(),
|
|
1117
|
-
colour1: z.string(),
|
|
1118
|
-
colour2: z.string(),
|
|
1119
|
-
})
|
|
1120
|
-
.optional(),
|
|
1121
|
-
quality: z.number().optional(),
|
|
1122
|
-
formatQuality: z.record(z.string(), z.number()).optional(),
|
|
1123
|
-
autoquality: z
|
|
1124
|
-
.object({
|
|
1125
|
-
method: z.enum(["dssim", "size"]),
|
|
1126
|
-
target: z.number(),
|
|
1127
|
-
min: z.number(),
|
|
1128
|
-
max: z.number(),
|
|
1129
|
-
allowedError: z.number(),
|
|
1130
|
-
})
|
|
1131
|
-
.optional(),
|
|
1132
|
-
maxBytes: z.number().optional(),
|
|
1133
|
-
jpegOptions: z
|
|
1134
|
-
.object({
|
|
1135
|
-
progressive: z.boolean(),
|
|
1136
|
-
noSubsample: z.boolean(),
|
|
1137
|
-
trellisQuant: z.boolean(),
|
|
1138
|
-
overshootDeringing: z.boolean(),
|
|
1139
|
-
optimizeScans: z.boolean(),
|
|
1140
|
-
quantTable: z.number().optional(),
|
|
1141
|
-
})
|
|
1142
|
-
.optional(),
|
|
1143
|
-
pngOptions: z
|
|
1144
|
-
.object({
|
|
1145
|
-
interlaced: z.boolean(),
|
|
1146
|
-
quantize: z.boolean(),
|
|
1147
|
-
quantizationColours: z.number().optional(),
|
|
1148
|
-
})
|
|
1149
|
-
.optional(),
|
|
1150
|
-
webpOptions: z
|
|
1151
|
-
.object({
|
|
1152
|
-
compression: z.number().optional(),
|
|
1153
|
-
smartSubsample: z.boolean(),
|
|
1154
|
-
preset: z.string().optional(),
|
|
1155
|
-
})
|
|
1156
|
-
.optional(),
|
|
1157
|
-
avifOptions: z
|
|
1158
|
-
.object({
|
|
1159
|
-
subsample: z.string().optional(),
|
|
1160
|
-
})
|
|
1161
|
-
.optional(),
|
|
1162
|
-
blur: z.number().optional(),
|
|
1163
|
-
sharpen: z.number().optional(),
|
|
1164
|
-
pixelate: z.number().optional(),
|
|
1165
|
-
unsharpMasking: z
|
|
1166
|
-
.object({ mode: z.string(), weight: z.number(), divider: z.number() })
|
|
1167
|
-
.optional(),
|
|
1168
|
-
colorize: z
|
|
1169
|
-
.object({
|
|
1170
|
-
opacity: z.number(),
|
|
1171
|
-
colour: z.string(),
|
|
1172
|
-
keepAlpha: z.boolean(),
|
|
1173
|
-
})
|
|
1174
|
-
.optional(),
|
|
1175
|
-
gradient: z
|
|
1176
|
-
.object({
|
|
1177
|
-
opacity: z.number(),
|
|
1178
|
-
colour: z.string(),
|
|
1179
|
-
direction: z.string(),
|
|
1180
|
-
start: z.number(),
|
|
1181
|
-
stop: z.number(),
|
|
1182
|
-
})
|
|
1183
|
-
.optional(),
|
|
1184
|
-
rotate: z.number().optional(),
|
|
1185
|
-
flip: z.object({ horizontal: z.boolean(), vertical: z.boolean() }).optional(),
|
|
1186
|
-
autoRotate: z.boolean().optional(),
|
|
1187
|
-
background: rgb.optional(),
|
|
1188
|
-
backgroundAlpha: z.number().optional(),
|
|
1189
|
-
padding: sides.optional(),
|
|
1190
|
-
stripMetadata: z.boolean().optional(),
|
|
1191
|
-
keepCopyright: z.boolean().optional(),
|
|
1192
|
-
stripColorProfile: z.boolean().optional(),
|
|
1193
|
-
dpi: z.number().optional(),
|
|
1194
|
-
enforceThumbnail: z.boolean().optional(),
|
|
1195
|
-
videoThumbnailSecond: z.number().optional(),
|
|
1196
|
-
videoThumbnailKeyframes: z.boolean().optional(),
|
|
1197
|
-
videoThumbnailAnimation: z
|
|
1198
|
-
.object({
|
|
1199
|
-
step: z.number(),
|
|
1200
|
-
delay: z.number(),
|
|
1201
|
-
frames: z.number(),
|
|
1202
|
-
frameWidth: z.number(),
|
|
1203
|
-
frameHeight: z.number(),
|
|
1204
|
-
extendFrame: z.boolean(),
|
|
1205
|
-
trim: z.boolean(),
|
|
1206
|
-
fill: z.boolean(),
|
|
1207
|
-
focusX: z.number(),
|
|
1208
|
-
focusY: z.number(),
|
|
1209
|
-
})
|
|
1210
|
-
.optional(),
|
|
1211
|
-
crop: z
|
|
1212
|
-
.object({
|
|
1213
|
-
width: z.number(),
|
|
1214
|
-
height: z.number(),
|
|
1215
|
-
gravity: gravitySchema.optional(),
|
|
1216
|
-
})
|
|
1217
|
-
.optional(),
|
|
1218
|
-
cropAspectRatio: z.number().optional(),
|
|
1219
|
-
gravity: gravitySchema.optional(),
|
|
1220
|
-
enlarge: z.boolean().optional(),
|
|
1221
|
-
bestFormat: z.boolean().optional(),
|
|
1222
|
-
skipProcessing: z.array(z.string()).optional(),
|
|
1223
|
-
raw: z.boolean().optional(),
|
|
1224
|
-
cacheBuster: z.string().optional(),
|
|
1225
|
-
expires: z.number().optional(),
|
|
1226
|
-
filename: z.string().optional(),
|
|
1227
|
-
returnAttachment: z.boolean().optional(),
|
|
1228
|
-
fallbackImageUrl: z.string().optional(),
|
|
1229
|
-
hashsum: z.object({ type: z.string(), hash: z.string() }).optional(),
|
|
1230
|
-
maxSrcResolution: z.number().optional(),
|
|
1231
|
-
maxSrcFileSize: z.number().optional(),
|
|
1232
|
-
maxAnimationFrames: z.number().optional(),
|
|
1233
|
-
maxAnimationFrameResolution: z.number().optional(),
|
|
1234
|
-
maxResultDimension: z.number().optional(),
|
|
1235
|
-
}) satisfies z.ZodType<ParsedUrlInput>;
|
|
1236
|
-
|
|
1237
|
-
/** Fully parsed URL with all processing options validated. */
|
|
1238
|
-
export type ParsedUrl = z.output<typeof parsedUrlSchema>;
|
|
1239
|
-
|
|
1240
|
-
export type ImageUrl = ParsedUrl & { outputFormat: ImageFormat };
|
|
1241
|
-
export type VideoUrl = ParsedUrl & { outputFormat: VideoFormat };
|
|
1242
|
-
|
|
1243
|
-
export function isImageUrl(parsed: ParsedUrl): parsed is ImageUrl {
|
|
1244
|
-
if (IMAGE_FORMATS.has(parsed.outputFormat)) return true;
|
|
1245
|
-
if (VIDEO_FORMATS.has(parsed.outputFormat)) return false;
|
|
1246
|
-
// Video thumbnail options produce an image even from a video source
|
|
1247
|
-
if (
|
|
1248
|
-
parsed.videoThumbnailSecond !== undefined ||
|
|
1249
|
-
parsed.videoThumbnailAnimation !== undefined
|
|
1250
|
-
)
|
|
1251
|
-
return true;
|
|
1252
|
-
if (parsed.framerate !== undefined || parsed.cut !== undefined) return false;
|
|
1253
|
-
if (IMAGE_EXTENSIONS.test(parsed.sourceUrl)) return true;
|
|
1254
|
-
return false;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
export function isVideoUrl(parsed: ParsedUrl): parsed is VideoUrl {
|
|
1258
|
-
return !isImageUrl(parsed);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
export interface ParseOptions {
|
|
1262
|
-
/** AES-256-CBC key for decrypting `/enc/` source URLs. */
|
|
1263
|
-
encryptionKey?: Buffer;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
/** Parses an imgproxy-format processing path (after signature has been stripped). Supports `/<options>/plain/<source_url>[@<format>]` and `/<options>/enc/<encrypted_source_url>[@<format>]`. */
|
|
1267
|
-
export function parseProcessingUrl(
|
|
1268
|
-
path: string,
|
|
1269
|
-
options?: ParseOptions,
|
|
1270
|
-
): ParsedUrl {
|
|
1271
|
-
const withoutPrefix = path.replace(/^\//, "");
|
|
1272
|
-
|
|
1273
|
-
const plainIdx = withoutPrefix.indexOf("/plain/");
|
|
1274
|
-
const encIdx = withoutPrefix.indexOf("/enc/");
|
|
1275
|
-
|
|
1276
|
-
let optionsPart: string;
|
|
1277
|
-
let sourceUrl: string;
|
|
1278
|
-
let encrypted = false;
|
|
1279
|
-
|
|
1280
|
-
if (plainIdx !== -1) {
|
|
1281
|
-
optionsPart = withoutPrefix.slice(0, plainIdx);
|
|
1282
|
-
sourceUrl = withoutPrefix.slice(plainIdx + "/plain/".length);
|
|
1283
|
-
} else if (encIdx !== -1) {
|
|
1284
|
-
optionsPart = withoutPrefix.slice(0, encIdx);
|
|
1285
|
-
sourceUrl = withoutPrefix.slice(encIdx + "/enc/".length);
|
|
1286
|
-
encrypted = true;
|
|
1287
|
-
} else {
|
|
1288
|
-
throw new Error(
|
|
1289
|
-
"Unsupported URL format: expected /plain/ or /enc/ source URL",
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (!sourceUrl) {
|
|
1294
|
-
throw new Error("Missing source URL");
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// Parse @format suffix from source URL
|
|
1298
|
-
let format: OutputFormat = "mp4";
|
|
1299
|
-
let hasFormatSuffix = false;
|
|
1300
|
-
let bestFormatSuffix = false;
|
|
1301
|
-
const formatMatch = sourceUrl.match(/@([a-z0-9]+)$/);
|
|
1302
|
-
if (formatMatch) {
|
|
1303
|
-
let fmt = formatMatch[1];
|
|
1304
|
-
if (fmt === "jpeg") fmt = "jpg";
|
|
1305
|
-
if (fmt === "best") {
|
|
1306
|
-
bestFormatSuffix = true;
|
|
1307
|
-
sourceUrl = sourceUrl.slice(0, -formatMatch[0].length);
|
|
1308
|
-
} else if (ALL_FORMATS.has(fmt)) {
|
|
1309
|
-
format = fmt as OutputFormat;
|
|
1310
|
-
sourceUrl = sourceUrl.slice(0, -formatMatch[0].length);
|
|
1311
|
-
hasFormatSuffix = true;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
if (encrypted) {
|
|
1316
|
-
if (!options?.encryptionKey) {
|
|
1317
|
-
throw new HTTPError(
|
|
1318
|
-
"Encrypted source URLs are not supported: no encryption key provided",
|
|
1319
|
-
{ code: "BAD_REQUEST" },
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
sourceUrl = decryptSourceUrl(sourceUrl, options.encryptionKey);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
// Parse option segments into a { name: value } record, then validate with Zod
|
|
1326
|
-
const raw = Object.fromEntries(
|
|
1327
|
-
optionsPart
|
|
1328
|
-
.split("/")
|
|
1329
|
-
.filter(Boolean)
|
|
1330
|
-
.map((segment) => {
|
|
1331
|
-
const idx = segment.indexOf(":");
|
|
1332
|
-
if (idx === -1) return [segment, ""];
|
|
1333
|
-
const name = segment.slice(0, idx);
|
|
1334
|
-
const value = segment.slice(idx + 1);
|
|
1335
|
-
return [SHORTHANDS[name] ?? name, value];
|
|
1336
|
-
}),
|
|
1337
|
-
);
|
|
1338
|
-
|
|
1339
|
-
const parsedOptions = optionsSchema.parse(raw);
|
|
1340
|
-
|
|
1341
|
-
if (parsedOptions.formatOverride) {
|
|
1342
|
-
format = parsedOptions.formatOverride;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
if (!hasFormatSuffix && !parsedOptions.formatOverride) {
|
|
1346
|
-
if (IMAGE_EXTENSIONS.test(sourceUrl)) {
|
|
1347
|
-
format = "jpg";
|
|
1348
|
-
}
|
|
1349
|
-
// Video thumbnail options produce an image — default to jpg if no explicit image format
|
|
1350
|
-
if (
|
|
1351
|
-
(parsedOptions.videoThumbnailSecond !== undefined ||
|
|
1352
|
-
parsedOptions.videoThumbnailAnimation !== undefined) &&
|
|
1353
|
-
VIDEO_FORMATS.has(format)
|
|
1354
|
-
) {
|
|
1355
|
-
format = "jpg";
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
const parsed = parsedUrlSchema.parse({
|
|
1360
|
-
resize: parsedOptions.resize,
|
|
1361
|
-
resizingAlgorithm: parsedOptions.resizingAlgorithm,
|
|
1362
|
-
sourceUrl,
|
|
1363
|
-
outputFormat: format,
|
|
1364
|
-
minWidth: parsedOptions.minWidth,
|
|
1365
|
-
minHeight: parsedOptions.minHeight,
|
|
1366
|
-
extend: parsedOptions.extend,
|
|
1367
|
-
extendAspectRatio: parsedOptions.extendAspectRatio,
|
|
1368
|
-
framerate: parsedOptions.framerate,
|
|
1369
|
-
cut: parsedOptions.cut,
|
|
1370
|
-
mute: parsedOptions.mute,
|
|
1371
|
-
trim: parsedOptions.trim,
|
|
1372
|
-
brightness: parsedOptions.brightness,
|
|
1373
|
-
contrast: parsedOptions.contrast,
|
|
1374
|
-
saturation: parsedOptions.saturation,
|
|
1375
|
-
monochrome: parsedOptions.monochrome,
|
|
1376
|
-
duotone: parsedOptions.duotone,
|
|
1377
|
-
quality: parsedOptions.quality,
|
|
1378
|
-
formatQuality: parsedOptions.formatQuality,
|
|
1379
|
-
autoquality: parsedOptions.autoquality,
|
|
1380
|
-
maxBytes: parsedOptions.maxBytes,
|
|
1381
|
-
jpegOptions: parsedOptions.jpegOptions,
|
|
1382
|
-
pngOptions: parsedOptions.pngOptions,
|
|
1383
|
-
webpOptions: parsedOptions.webpOptions,
|
|
1384
|
-
avifOptions: parsedOptions.avifOptions,
|
|
1385
|
-
blur: parsedOptions.blur,
|
|
1386
|
-
sharpen: parsedOptions.sharpen,
|
|
1387
|
-
pixelate: parsedOptions.pixelate,
|
|
1388
|
-
unsharpMasking: parsedOptions.unsharpMasking,
|
|
1389
|
-
colorize: parsedOptions.colorize,
|
|
1390
|
-
gradient: parsedOptions.gradient,
|
|
1391
|
-
rotate: parsedOptions.rotate,
|
|
1392
|
-
flip: parsedOptions.flip,
|
|
1393
|
-
autoRotate: parsedOptions.autoRotate,
|
|
1394
|
-
background: parsedOptions.background,
|
|
1395
|
-
backgroundAlpha: parsedOptions.backgroundAlpha,
|
|
1396
|
-
padding: parsedOptions.padding,
|
|
1397
|
-
stripMetadata: parsedOptions.stripMetadata,
|
|
1398
|
-
keepCopyright: parsedOptions.keepCopyright,
|
|
1399
|
-
stripColorProfile: parsedOptions.stripColorProfile,
|
|
1400
|
-
dpi: parsedOptions.dpi,
|
|
1401
|
-
enforceThumbnail: parsedOptions.enforceThumbnail,
|
|
1402
|
-
videoThumbnailSecond: parsedOptions.videoThumbnailSecond,
|
|
1403
|
-
videoThumbnailKeyframes: parsedOptions.videoThumbnailKeyframes,
|
|
1404
|
-
videoThumbnailAnimation: parsedOptions.videoThumbnailAnimation,
|
|
1405
|
-
crop: parsedOptions.crop,
|
|
1406
|
-
cropAspectRatio: parsedOptions.cropAspectRatio,
|
|
1407
|
-
gravity: parsedOptions.gravity,
|
|
1408
|
-
enlarge: parsedOptions.enlarge,
|
|
1409
|
-
bestFormat: parsedOptions.bestFormat || bestFormatSuffix || undefined,
|
|
1410
|
-
skipProcessing: parsedOptions.skipProcessing,
|
|
1411
|
-
raw: parsedOptions.raw,
|
|
1412
|
-
cacheBuster: parsedOptions.cacheBuster,
|
|
1413
|
-
expires: parsedOptions.expires,
|
|
1414
|
-
filename: parsedOptions.filename,
|
|
1415
|
-
returnAttachment: parsedOptions.returnAttachment,
|
|
1416
|
-
fallbackImageUrl: parsedOptions.fallbackImageUrl,
|
|
1417
|
-
hashsum: parsedOptions.hashsum,
|
|
1418
|
-
maxSrcResolution: parsedOptions.maxSrcResolution,
|
|
1419
|
-
maxSrcFileSize: parsedOptions.maxSrcFileSize,
|
|
1420
|
-
maxAnimationFrames: parsedOptions.maxAnimationFrames,
|
|
1421
|
-
maxAnimationFrameResolution: parsedOptions.maxAnimationFrameResolution,
|
|
1422
|
-
maxResultDimension: parsedOptions.maxResultDimension,
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
return parsed;
|
|
1426
|
-
}
|