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 CHANGED
@@ -9,37 +9,385 @@
9
9
 
10
10
  High performance, secure and easy-to-use image optimizer powered by [sharp](https://github.com/lovell/sharp) and [svgo](https://github.com/svg/svgo).
11
11
 
12
- Used by [Nuxt Image](https://image.nuxt.com/) and [Netlify](https://www.npmjs.com/package/@netlify/ipx) and open to everyone!
13
-
14
- ## Migration from v3 to v4
12
+ Point IPX at a directory or a list of allowed domains, and every image is available in any size, format and quality straight from its URL:
15
13
 
16
- > [!NOTE]
17
- > This is the active development branch for IPX v4. Check out [v3](https://github.com/unjs/ipx/tree/v3) for v3 docs.
14
+ ```
15
+ /w_512,f_webp/photos/buffalo.png
16
+ ```
18
17
 
19
- - The server creation APIs have changed. See the Programmatic API section for examples.
20
- - The JSON error format has changed from `{ error: string }` to `{ status, statusText, message }`.
18
+ Used by [Nuxt Image](https://image.nuxt.com/) and [Netlify](https://www.npmjs.com/package/@netlify/ipx) and open to everyone!
21
19
 
22
- ## Using CLI
20
+ > [!NOTE]
21
+ > This is the active development branch for IPX v4. Check out [v3](https://github.com/unjs/ipx/tree/v3) for v3 docs, and [Migration from v3 to v4](#migration-from-v3-to-v4) if you are upgrading.
23
22
 
24
- You can use `ipx` command to start server.
23
+ ## Quick Start
25
24
 
26
- Using `npx`:
25
+ Start a server for the images in the current directory with the `ipx` command:
27
26
 
28
27
  ```bash
29
28
  npx ipx serve --dir ./
30
29
  ```
31
30
 
32
- Using `bun`
31
+ Using `bun`:
33
32
 
34
33
  ```bash
35
34
  bunx ipx serve --dir ./
36
35
  ```
37
36
 
38
- The default serve directory is the current working directory.
37
+ Then open the printed URL and add modifiers to any image path, for example `http://localhost:3000/w_200/buffalo.png`.
38
+
39
+ To embed IPX in your own app instead, see the [Programmatic API](#programmatic-api).
40
+
41
+ ## Image URLs
42
+
43
+ A request URL is a list of modifiers, followed by the id of the source image:
44
+
45
+ ```
46
+ /<modifiers>/<id>
47
+ ```
48
+
49
+ Multiple modifiers are separated by `,` and their arguments by `_`. Use `_` alone when no modifier is needed.
50
+
51
+ | URL | Result |
52
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
53
+ | `/_/static/buffalo.png` | The original image. |
54
+ | `/w_200/static/buffalo.png` | Width set to `200`, original format (`png`) kept. |
55
+ | `/f_webp/static/buffalo.png` | Format changed to `webp`, everything else kept as in the source. |
56
+ | `/f_auto/static/buffalo.png` | Best format for the client (avif/webp/jpeg), negotiated from the browser's `accept` header. |
57
+ | `/s_200x200,fit_contain,f_webp/static/buffalo.png` | Resized to fit inside `200x200px` on a background canvas and converted to `webp`. |
58
+
59
+ The URL style is configurable, see [Custom URL Style](#custom-url-style).
60
+
61
+ ## Modifiers
62
+
63
+ Modifier arguments are separated with `_` and validated before they reach sharp. Invalid input is rejected with a `400 IPX_INVALID_MODIFIER_ARG` (or `400 IPX_MISSING_MODIFIER_ARG` for a required argument) instead of failing the request. Some arguments can only be validated by libvips once it runs, which happens after the whole pipeline is set up; those surface as a `400 IPX_INVALID_MODIFIER`. Trailing arguments may be omitted to keep the sharp default, except where noted.
64
+
65
+ Colours (`background`, `tint`) accept any colour sharp understands: hex (`f00`, `ff0000`, `ff000080`) with an optional leading `#` — which cannot be used inside a URL path, so it may be dropped — or a CSS colour name (`red`). Boolean arguments accept `true` / `false` as well as the shorter `1` / `0`.
66
+
67
+ <!-- automd:ipx-operations -->
68
+
69
+ <table>
70
+ <tr>
71
+ <td valign="top" width="50%">
72
+ <div align="center"><img src="./assets/operations/width.jpg" alt="/w_160/sample.jpg"></div>
73
+ <div align="center"><b><code>width</code> / <code>w</code></b></div>
74
+ <code>/w_160/sample.jpg</code>
75
+ <br>
76
+ Positive integer. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
77
+ </td>
78
+ <td valign="top" width="50%">
79
+ <div align="center"><img src="./assets/operations/height.jpg" alt="/h_120/sample.jpg"></div>
80
+ <div align="center"><b><code>height</code> / <code>h</code></b></div>
81
+ <code>/h_120/sample.jpg</code>
82
+ <br>
83
+ Positive integer. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
84
+ </td>
85
+ </tr>
86
+ <tr>
87
+ <td valign="top" width="50%">
88
+ <div align="center"><img src="./assets/operations/resize.jpg" alt="/s_200x200/sample.jpg"></div>
89
+ <div align="center"><b><code>resize</code> / <code>s</code></b></div>
90
+ <code>/s_200x200/sample.jpg</code>
91
+ <br>
92
+ <code>{width}x{height}</code> of positive integers. A single value (<code>s_200</code>) is a square. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
93
+ </td>
94
+ <td valign="top" width="50%">
95
+ <div align="center"><img src="./assets/operations/kernel.jpg" alt="/s_80x80,kernel_nearest/sample.jpg"></div>
96
+ <div align="center"><b><code>kernel</code></b></div>
97
+ <code>/s_80x80,kernel_nearest/sample.jpg</code>
98
+ <br>
99
+ Sets <code>kernel</code> option for <code>resize</code>. One of <code>nearest</code>, <code>linear</code>, <code>cubic</code>, <code>mitchell</code>, <code>lanczos2</code>, <code>lanczos3</code> (default), <code>mks2013</code> or <code>mks2021</code>. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
100
+ </td>
101
+ </tr>
102
+ <tr>
103
+ <td valign="top" width="50%">
104
+ <div align="center"><img src="./assets/operations/fit.jpg" alt="/s_200x200,fit_contain,b_00ff00/sample.jpg"></div>
105
+ <div align="center"><b><code>fit</code></b></div>
106
+ <code>/s_200x200,fit_contain,b_00ff00/sample.jpg</code>
107
+ <br>
108
+ Sets <code>fit</code> option for <code>resize</code>. One of <code>contain</code>, <code>cover</code> (default), <code>fill</code>, <code>inside</code> or <code>outside</code>. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
109
+ </td>
110
+ <td valign="top" width="50%">
111
+ <div align="center"><img src="./assets/operations/position.jpg" alt="/s_200x200,pos_top/sample.jpg"></div>
112
+ <div align="center"><b><code>position</code> / <code>pos</code></b></div>
113
+ <code>/s_200x200,pos_top/sample.jpg</code>
114
+ <br>
115
+ Sets <code>position</code> option for <code>resize</code>. A position (<code>top</code>, <code>right top</code>, <code>right</code>, <code>right bottom</code>, <code>bottom</code>, <code>left bottom</code>, <code>left</code>, <code>left top</code>), gravity (<code>north</code>, <code>northeast</code>, ..., <code>center</code>) or strategy (<code>entropy</code>, <code>attention</code>). Since <code>_</code> separates arguments, multi-word values use <code>-</code> (<code>pos_right-top</code>). The numeric gravity (<code>0</code>-<code>8</code>) and strategy (<code>16</code>-<code>17</code>) constants are also accepted. (<a href="https://sharp.pixelplumbing.com/api-resize#resize">docs</a>)
116
+ </td>
117
+ </tr>
118
+ <tr>
119
+ <td valign="top" width="50%">
120
+ <div align="center"><img src="./assets/operations/extend.jpg" alt="/extend_20_40_20_40_mirror/sample.jpg"></div>
121
+ <div align="center"><b><code>extend</code></b></div>
122
+ <code>/extend_20_40_20_40_mirror/sample.jpg</code>
123
+ <br>
124
+ Extend / pad / extrude one or more edges of the image. Format: <code>extend_{top}_{right}_{bottom}_{left}_{extendWith}</code>. Edges are integers between <code>0</code> and <code>10000</code>. Optional <code>extendWith</code>: <code>background</code> (default), <code>copy</code>, <code>repeat</code> or <code>mirror</code>. When extending with <code>background</code>, the colour comes from the <code>background</code> / <code>b</code> modifier, e.g. <code>b_00ff00,extend_20_40_20_40</code>. (<a href="https://sharp.pixelplumbing.com/api-resize#extend">docs</a>)
125
+ </td>
126
+ <td valign="top" width="50%">
127
+ <div align="center"><img src="./assets/operations/background.jpg" alt="/rotate_45,b_00ff00/sample.jpg"></div>
128
+ <div align="center"><b><code>background</code> / <code>b</code></b></div>
129
+ <code>/rotate_45,b_00ff00/sample.jpg</code>
130
+ <br>
131
+ Background colour used by <code>extend</code>, <code>rotate</code>, <code>flatten</code>, <code>opacity</code> and <code>resize</code> (with <code>fit_contain</code>).
132
+ </td>
133
+ </tr>
134
+ <tr>
135
+ <td valign="top" width="50%">
136
+ <div align="center"><img src="./assets/operations/extract.jpg" alt="/extract_60_20_180_140/sample.jpg"></div>
137
+ <div align="center"><b><code>extract</code> / <code>crop</code></b></div>
138
+ <code>/extract_60_20_180_140/sample.jpg</code>
139
+ <br>
140
+ Extract/crop a region of the image, as <code>extract_{left}_{top}_{width}_{height}</code>. All four arguments are required; <code>width</code> and <code>height</code> must be positive. (<a href="https://sharp.pixelplumbing.com/api-resize#extract">docs</a>)
141
+ </td>
142
+ <td valign="top" width="50%">
143
+ <div align="center"><img src="./assets/operations/format.webp" alt="/f_webp/sample.jpg"></div>
144
+ <div align="center"><b><code>format</code> / <code>f</code></b></div>
145
+ <code>/f_webp/sample.jpg</code>
146
+ <br>
147
+ Supported formats: <code>jpg</code>, <code>jpeg</code>, <code>png</code>, <code>webp</code>, <code>avif</code>, <code>gif</code>, <code>heif</code>, <code>tiff</code> and <code>auto</code> (experimental, only with middleware). (<a href="https://sharp.pixelplumbing.com/api-output#toformat">docs</a>)
148
+ </td>
149
+ </tr>
150
+ <tr>
151
+ <td valign="top" width="50%">
152
+ <div align="center"><img src="./assets/operations/quality.jpg" alt="/q_10/sample.jpg"></div>
153
+ <div align="center"><b><code>quality</code> / <code>q</code></b></div>
154
+ <code>/q_10/sample.jpg</code>
155
+ <br>
156
+ Integer between <code>1</code> and <code>100</code>.
157
+ </td>
158
+ <td valign="top" width="50%">
159
+ <div align="center"><img src="./assets/operations/rotate.jpg" alt="/rotate_45/sample.jpg"></div>
160
+ <div align="center"><b><code>rotate</code></b></div>
161
+ <code>/rotate_45/sample.jpg</code>
162
+ <br>
163
+ Angle in degrees, between <code>-3600</code> and <code>3600</code>. Angles that are not a multiple of <code>90</code> are filled with the <code>background</code> / <code>b</code> colour. Without an angle (<code>rotate</code>) the image is auto-oriented from its EXIF tag. (<a href="https://sharp.pixelplumbing.com/api-operation#rotate">docs</a>)
164
+ </td>
165
+ </tr>
166
+ <tr>
167
+ <td valign="top" width="50%">
168
+ <div align="center"><img src="./assets/operations/enlarge.jpg" alt="/enlarge,s_400x400/sample.jpg"></div>
169
+ <div align="center"><b><code>enlarge</code></b></div>
170
+ <code>/enlarge,s_400x400/sample.jpg</code>
171
+ <br>
172
+ Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio.
173
+ </td>
174
+ <td valign="top" width="50%">
175
+ <div align="center"><img src="./assets/operations/flip.jpg" alt="/flip/sample.jpg"></div>
176
+ <div align="center"><b><code>flip</code></b></div>
177
+ <code>/flip/sample.jpg</code>
178
+ <br>
179
+ Mirror the image vertically. (<a href="https://sharp.pixelplumbing.com/api-operation#flip">docs</a>)
180
+ </td>
181
+ </tr>
182
+ <tr>
183
+ <td valign="top" width="50%">
184
+ <div align="center"><img src="./assets/operations/flop.jpg" alt="/flop/sample.jpg"></div>
185
+ <div align="center"><b><code>flop</code></b></div>
186
+ <code>/flop/sample.jpg</code>
187
+ <br>
188
+ Mirror the image horizontally. (<a href="https://sharp.pixelplumbing.com/api-operation#flop">docs</a>)
189
+ </td>
190
+ <td valign="top" width="50%">
191
+ <div align="center"><img src="./assets/operations/sharpen.jpg" alt="/sharpen_5/sample.jpg"></div>
192
+ <div align="center"><b><code>sharpen</code></b></div>
193
+ <code>/sharpen_5/sample.jpg</code>
194
+ <br>
195
+ <code>{sigma}_{flat}_{jagged}_{x1}_{y2}_{y3}</code>. <code>sigma</code> is between <code>0.000001</code> and <code>10</code>, the rest between <code>0</code> and <code>1000000</code>. Without arguments (<code>sharpen</code>) a mild sharpen is applied. (<a href="https://sharp.pixelplumbing.com/api-operation#sharpen">docs</a>)
196
+ </td>
197
+ </tr>
198
+ <tr>
199
+ <td valign="top" width="50%">
200
+ <div align="center"><img src="./assets/operations/median.jpg" alt="/median_10/sample.jpg"></div>
201
+ <div align="center"><b><code>median</code></b></div>
202
+ <code>/median_10/sample.jpg</code>
203
+ <br>
204
+ Square mask size, an integer between <code>1</code> and <code>1000</code> (defaults to <code>3</code>). (<a href="https://sharp.pixelplumbing.com/api-operation#median">docs</a>)
205
+ </td>
206
+ <td valign="top" width="50%">
207
+ <div align="center"><img src="./assets/operations/blur.jpg" alt="/blur_5/sample.jpg"></div>
208
+ <div align="center"><b><code>blur</code></b></div>
209
+ <code>/blur_5/sample.jpg</code>
210
+ <br>
211
+ <code>{sigma}_{precision}_{minAmplitude}</code>. <code>sigma</code> is a number between <code>0.3</code> and <code>1000</code>, <code>precision</code> one of <code>integer</code> (default), <code>float</code> or <code>approximate</code>, <code>minAmplitude</code> a number between <code>0.001</code> and <code>1</code>. Without arguments (<code>blur</code>) a mild blur is applied. (<a href="https://sharp.pixelplumbing.com/api-operation#blur">docs</a>)
212
+ </td>
213
+ </tr>
214
+ <tr>
215
+ <td valign="top" width="50%">
216
+ <div align="center"><img src="./assets/operations/dilate.png" alt="/dilate_5/sample-logo.png"></div>
217
+ <div align="center"><b><code>dilate</code></b></div>
218
+ <code>/dilate_5/sample-logo.png</code>
219
+ <br>
220
+ Expand foreground objects. Width in pixels, an integer between <code>1</code> and <code>100</code> (defaults to <code>1</code>). Capped below the sharp maximum, since the cost grows with the width. (<a href="https://sharp.pixelplumbing.com/api-operation#dilate">docs</a>)
221
+ </td>
222
+ <td valign="top" width="50%">
223
+ <div align="center"><img src="./assets/operations/erode.png" alt="/erode_5/sample-logo.png"></div>
224
+ <div align="center"><b><code>erode</code></b></div>
225
+ <code>/erode_5/sample-logo.png</code>
226
+ <br>
227
+ Shrink foreground objects. Width in pixels, an integer between <code>1</code> and <code>100</code> (defaults to <code>1</code>). Capped below the sharp maximum, since the cost grows with the width. (<a href="https://sharp.pixelplumbing.com/api-operation#erode">docs</a>)
228
+ </td>
229
+ </tr>
230
+ <tr>
231
+ <td valign="top" width="50%">
232
+ <div align="center"><img src="./assets/operations/clahe.jpg" alt="/clahe_20_20_5/sample.jpg"></div>
233
+ <div align="center"><b><code>clahe</code></b></div>
234
+ <code>/clahe_20_20_5/sample.jpg</code>
235
+ <br>
236
+ Contrast limiting adaptive histogram equalization, as <code>{width}_{height}_{maxSlope}</code>. <code>width</code> is required and <code>height</code> defaults to it (a square window); both are integers between <code>1</code> and <code>100</code>, capped below the sharp maximum since the cost grows with the window, and clamped to the source dimensions. <code>maxSlope</code> is an integer between <code>0</code> and <code>100</code> (defaults to <code>3</code>). (<a href="https://sharp.pixelplumbing.com/api-operation#clahe">docs</a>)
237
+ </td>
238
+ <td valign="top" width="50%">
239
+ <div align="center"><img src="./assets/operations/flatten.jpg" alt="/flatten,b_00ff00,f_jpeg/sample-alpha.png"></div>
240
+ <div align="center"><b><code>flatten</code></b></div>
241
+ <code>/flatten,b_00ff00,f_jpeg/sample-alpha.png</code>
242
+ <br>
243
+ Remove alpha channel, if any, and replace with the <code>background</code> / <code>b</code> colour. (<a href="https://sharp.pixelplumbing.com/api-operation#flatten">docs</a>)
244
+ </td>
245
+ </tr>
246
+ <tr>
247
+ <td valign="top" width="50%">
248
+ <div align="center"><img src="./assets/operations/unflatten.webp" alt="/unflatten,f_webp/sample-logo.png"></div>
249
+ <div align="center"><b><code>unflatten</code></b></div>
250
+ <code>/unflatten,f_webp/sample-logo.png</code>
251
+ <br>
252
+ Make every fully white pixel transparent, so the output format needs an alpha channel (<code>png</code>, <code>webp</code>, <code>avif</code>). (<a href="https://sharp.pixelplumbing.com/api-operation#unflatten">docs</a>)
253
+ </td>
254
+ <td valign="top" width="50%">
255
+ <div align="center"><img src="./assets/operations/gamma.jpg" alt="/gamma_3/sample.jpg"></div>
256
+ <div align="center"><b><code>gamma</code></b></div>
257
+ <code>/gamma_3/sample.jpg</code>
258
+ <br>
259
+ <code>{gamma}_{gammaOut}</code>, each a number between <code>1.0</code> and <code>3.0</code> (defaults to <code>2.2</code>). (<a href="https://sharp.pixelplumbing.com/api-operation#gamma">docs</a>)
260
+ </td>
261
+ </tr>
262
+ <tr>
263
+ <td valign="top" width="50%">
264
+ <div align="center"><img src="./assets/operations/negate.jpg" alt="/negate/sample.jpg"></div>
265
+ <div align="center"><b><code>negate</code></b></div>
266
+ <code>/negate/sample.jpg</code>
267
+ <br>
268
+ Optional <code>{alpha}</code> (<code>true</code> by default) controls whether the alpha channel is negated too, e.g. <code>negate_false</code>. (<a href="https://sharp.pixelplumbing.com/api-operation#negate">docs</a>)
269
+ </td>
270
+ <td valign="top" width="50%">
271
+ <div align="center"><img src="./assets/operations/normalize.jpg" alt="/normalize_10_90/sample.jpg"></div>
272
+ <div align="center"><b><code>normalize</code></b></div>
273
+ <code>/normalize_10_90/sample.jpg</code>
274
+ <br>
275
+ Stretch the luminance to the full dynamic range, as <code>{lower}_{upper}</code> percentiles. <code>lower</code> is a number between <code>0</code> and <code>99</code> (defaults to <code>1</code>), <code>upper</code> between <code>1</code> and <code>100</code> (defaults to <code>99</code>) and has to be greater than <code>lower</code>. (<a href="https://sharp.pixelplumbing.com/api-operation#normalize">docs</a>)
276
+ </td>
277
+ </tr>
278
+ <tr>
279
+ <td valign="top" width="50%">
280
+ <div align="center"><img src="./assets/operations/threshold.jpg" alt="/threshold_128/sample.jpg"></div>
281
+ <div align="center"><b><code>threshold</code></b></div>
282
+ <code>/threshold_128/sample.jpg</code>
283
+ <br>
284
+ <code>{threshold}_{grayscale}</code>. <code>threshold</code> is an integer between <code>0</code> and <code>255</code> (defaults to <code>128</code>). Optional <code>grayscale</code> (<code>true</code> by default) converts to single channel grayscale first, e.g. <code>threshold_128_false</code>. (<a href="https://sharp.pixelplumbing.com/api-operation#threshold">docs</a>)
285
+ </td>
286
+ <td valign="top" width="50%">
287
+ <div align="center"><img src="./assets/operations/linear.jpg" alt="/linear_1.5_-30/sample.jpg"></div>
288
+ <div align="center"><b><code>linear</code></b></div>
289
+ <code>/linear_1.5_-30/sample.jpg</code>
290
+ <br>
291
+ Levels adjustment applying <code>a * input + b</code>, as <code>{a}_{b}</code>. <code>a</code> is the multiplier (defaults to <code>1</code>), <code>b</code> the offset (defaults to <code>0</code>). (<a href="https://sharp.pixelplumbing.com/api-operation#linear">docs</a>)
292
+ </td>
293
+ </tr>
294
+ <tr>
295
+ <td valign="top" width="50%">
296
+ <div align="center"><img src="./assets/operations/tint.jpg" alt="/tint_00ff00/sample.jpg"></div>
297
+ <div align="center"><b><code>tint</code></b></div>
298
+ <code>/tint_00ff00/sample.jpg</code>
299
+ <br>
300
+ Tint colour. (<a href="https://sharp.pixelplumbing.com/api-colour#tint">docs</a>)
301
+ </td>
302
+ <td valign="top" width="50%">
303
+ <div align="center"><img src="./assets/operations/grayscale.jpg" alt="/grayscale/sample.jpg"></div>
304
+ <div align="center"><b><code>grayscale</code></b></div>
305
+ <code>/grayscale/sample.jpg</code>
306
+ <br>
307
+ Convert to 8-bit greyscale. (<a href="https://sharp.pixelplumbing.com/api-colour#grayscale">docs</a>)
308
+ </td>
309
+ </tr>
310
+ <tr>
311
+ <td valign="top" width="50%">
312
+ <div align="center"><img src="./assets/operations/modulate.jpg" alt="/modulate_1.5_2_90_10/sample.jpg"></div>
313
+ <div align="center"><b><code>modulate</code></b></div>
314
+ <code>/modulate_1.5_2_90_10/sample.jpg</code>
315
+ <br>
316
+ Transforms the image using <code>{brightness}_{saturation}_{hue}_{lightness}</code>. <code>brightness</code> and <code>saturation</code> are numbers <code>&gt;= 0</code>, <code>hue</code> is an integer in degrees. Each is also available on its own (below), which is easier to read when only one is needed. (<a href="https://sharp.pixelplumbing.com/api-operation#modulate">docs</a>)
317
+ </td>
318
+ <td valign="top" width="50%">
319
+ <div align="center"><img src="./assets/operations/brightness.jpg" alt="/brightness_1.5/sample.jpg"></div>
320
+ <div align="center"><b><code>brightness</code></b></div>
321
+ <code>/brightness_1.5/sample.jpg</code>
322
+ <br>
323
+ Brightness multiplier, a number <code>&gt;= 0</code>. Required. (<a href="https://sharp.pixelplumbing.com/api-operation#modulate">docs</a>)
324
+ </td>
325
+ </tr>
326
+ <tr>
327
+ <td valign="top" width="50%">
328
+ <div align="center"><img src="./assets/operations/saturation.jpg" alt="/saturation_0.3/sample.jpg"></div>
329
+ <div align="center"><b><code>saturation</code></b></div>
330
+ <code>/saturation_0.3/sample.jpg</code>
331
+ <br>
332
+ Saturation multiplier, a number <code>&gt;= 0</code>. Required. (<a href="https://sharp.pixelplumbing.com/api-operation#modulate">docs</a>)
333
+ </td>
334
+ <td valign="top" width="50%">
335
+ <div align="center"><img src="./assets/operations/hue.jpg" alt="/hue_90/sample.jpg"></div>
336
+ <div align="center"><b><code>hue</code></b></div>
337
+ <code>/hue_90/sample.jpg</code>
338
+ <br>
339
+ Hue rotation, an integer in degrees. Required. (<a href="https://sharp.pixelplumbing.com/api-operation#modulate">docs</a>)
340
+ </td>
341
+ </tr>
342
+ <tr>
343
+ <td valign="top" width="50%">
344
+ <div align="center"><img src="./assets/operations/lightness.jpg" alt="/lightness_30/sample.jpg"></div>
345
+ <div align="center"><b><code>lightness</code></b></div>
346
+ <code>/lightness_30/sample.jpg</code>
347
+ <br>
348
+ Lightness addend, a number. Required. (<a href="https://sharp.pixelplumbing.com/api-operation#modulate">docs</a>)
349
+ </td>
350
+ <td valign="top" width="50%">
351
+ <div align="center"><img src="./assets/operations/opacity.webp" alt="/opacity_0.5,f_webp/sample.jpg"></div>
352
+ <div align="center"><b><code>opacity</code></b></div>
353
+ <code>/opacity_0.5,f_webp/sample.jpg</code>
354
+ <br>
355
+ Opacity, a number between <code>0</code> and <code>1</code>. Required. The image is made transparent, so the output format needs an alpha channel (<code>png</code>, <code>webp</code>, <code>avif</code>). Set the <code>background</code> / <code>b</code> colour to blend into it instead and keep the output opaque, e.g. <code>opacity_0.5,b_fff,f_jpeg</code>.
356
+ </td>
357
+ </tr>
358
+ <tr>
359
+ <td valign="top" width="50%">
360
+ <div align="center"><img src="./assets/operations/animated.gif" alt="/a,w_160/sample.gif"></div>
361
+ <div align="center"><b><code>animated</code> / <code>a</code></b></div>
362
+ <code>/a,w_160/sample.gif</code>
363
+ <br>
364
+ Process every frame of an animated image. Experimental.
365
+ </td>
366
+ <td valign="top" width="50%">
367
+ <div align="center"><em>No sample: the sample source has no uniform border to trim.</em></div>
368
+ <div align="center"><b><code>trim</code></b></div>
369
+ <code>/trim_30/sample.jpg</code>
370
+ <br>
371
+ Trim pixels from the edges that are within the threshold of the top-left pixel colour. Threshold is a number <code>&gt;= 0</code> (defaults to <code>10</code>). (<a href="https://sharp.pixelplumbing.com/api-resize#trim">docs</a>)
372
+ </td>
373
+ </tr>
374
+ <tr>
375
+ <td valign="top" width="50%">
376
+ <div align="center"><em>No sample: the sample source carries no EXIF <code>Orientation</code> tag.</em></div>
377
+ <div align="center"><b><code>autoorient</code></b></div>
378
+ <code>/autoorient/sample.jpg</code>
379
+ <br>
380
+ Rotate and flip the image based on its EXIF <code>Orientation</code> tag, then remove the tag. (<a href="https://sharp.pixelplumbing.com/api-operation#autoorient">docs</a>)
381
+ </td>
382
+ <td valign="top" width="50%"></td>
383
+ </tr>
384
+ </table>
385
+
386
+ <!-- /automd -->
39
387
 
40
388
  ## Programmatic API
41
389
 
42
- You can use IPX as a middleware or directly use IPX interface.
390
+ Create an IPX instance with `createIPX()`, then either serve it directly or mount it as a handler in your own app.
43
391
 
44
392
  **Example:** Using built-in server
45
393
 
@@ -104,6 +452,8 @@ import {
104
452
  createIPXNodeHandler,
105
453
  } from "ipx";
106
454
 
455
+ import type { RequestHandler } from "express";
456
+
107
457
  const ipx = createIPX({
108
458
  storage: ipxFSStorage({ dir: "./public" }),
109
459
  httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
@@ -112,7 +462,7 @@ const ipx = createIPX({
112
462
 
113
463
  const app = Express();
114
464
 
115
- app.use("/ipx", createIPXNodeHandler(ipx));
465
+ app.use("/ipx", createIPXNodeHandler(ipx) as RequestHandler);
116
466
 
117
467
  // http://localhost:3000/ipx/w_512/picsum/1000
118
468
  app.listen(3000, () => {
@@ -122,101 +472,168 @@ app.listen(3000, () => {
122
472
 
123
473
  <!-- /automd -->
124
474
 
125
- ## URL Examples
475
+ ## Config
126
476
 
127
- Get original image:
477
+ Every option can also be set universally with an `IPX_*` environment variable, which is how the CLI is configured. Explicit options win over the environment.
128
478
 
129
- `/_/static/buffalo.png`
479
+ ### General
130
480
 
131
- Change format to `webp` and keep other things same as source:
481
+ | Option | Environment variable | Default | Description |
482
+ | -------------------- | -------------------------- | ------- | --------------------------------------------------------- |
483
+ | `alias` | `IPX_ALIAS` | `{}` | Map URL prefixes to other prefixes or remote origins. |
484
+ | `maxOutputDimension` | `IPX_MAX_OUTPUT_DIMENSION` | `8192` | Maximum width and height (in pixels) of the output image. |
132
485
 
133
- `/f_webp/static/buffalo.png`
486
+ Requested `width`, `height` and `resize` dimensions are clamped to `maxOutputDimension`, preserving the requested aspect ratio, and `extend` edges are clamped so the extended canvas stays within it. This bounds how much memory a single request can allocate: sharp only limits the _input_ size, so without it `/enlarge,s_20000x20000/image.jpg` (or `/extend_10000_10000_10000_10000/image.jpg`) allocates gigabytes from a small source image. Set to `false` to disable, which is only safe when modifiers come from a trusted source.
134
487
 
135
- Automatically convert to a preferred format (avif/webp/jpeg). Uses the browsers `accept` header to negotiate:
488
+ ### Filesystem source (`ipxFSStorage`)
136
489
 
137
- `/f_auto/static/buffalo.png`
490
+ Enabled by default with the CLI only.
138
491
 
139
- Keep original format (`png`) and set width to `200`:
492
+ | Option | Environment variable | Default | Description |
493
+ | ------------------------- | ----------------------------------- | ----------------------- | ------------------------------------------------------ |
494
+ | `dir` | `IPX_FS_DIR` | `.` (current directory) | Directory (or directories) files are served from. |
495
+ | `maxAge` | `IPX_FS_MAX_AGE` | `60` (via `maxAge`) | `cache-control` max-age, in seconds, for served files. |
496
+ | `allowSymlinksOutsideDir` | `IPX_FS_ALLOW_SYMLINKS_OUTSIDE_DIR` | `false` | Allows symlinks inside `dir` to resolve outside of it. |
140
497
 
141
- `/w_200/static/buffalo.png`
498
+ ### HTTP(s) source (`ipxHttpStorage`)
142
499
 
143
- Resize to `200x200px` using `embed` method and change format to `webp`:
500
+ Enabled by default with the CLI only.
144
501
 
145
- `/embed,f_webp,s_200x200/static/buffalo.png`
502
+ | Option | Environment variable | Default | Description |
503
+ | ----------------- | ---------------------------- | ------- | --------------------------------------------------------------- |
504
+ | `domains` | `IPX_HTTP_DOMAINS` | `[]` | Allowlist of hostnames images can be fetched from. |
505
+ | `maxAge` | `IPX_HTTP_MAX_AGE` | `300` | |
506
+ | `fetchOptions` | `IPX_HTTP_FETCH_OPTIONS` | `{}` | Passed to `fetch()`. |
507
+ | `allowAllDomains` | `IPX_HTTP_ALLOW_ALL_DOMAINS` | `false` | Disables the allowlist. Unsafe on a public server. |
508
+ | `blockPrivateIPs` | `IPX_HTTP_BLOCK_PRIVATE_IPS` | `false` | Rejects hosts that are, or resolve to, a non-public IP address. |
146
509
 
147
- ## Config
510
+ Only `http:` and `https:` URLs are allowed (anything else is rejected with `403 IPX_FORBIDDEN_PROTOCOL`), and redirects are followed **only within the allowlist**, up to 3 hops: each redirect target is re-validated and a redirect to a host that is not listed is rejected with `403 IPX_FORBIDDEN_HOST` (`502 IPX_TOO_MANY_REDIRECTS` beyond 3 hops). Previously redirects were followed blindly, which let an allowlisted host with an open redirect bounce IPX to internal addresses such as the cloud metadata service (SSRF). If an allowlisted host redirects to a CDN, add the CDN hostname to the allowlist as well. Redirect re-validation is skipped when `allowAllDomains` is enabled without `blockPrivateIPs` (nothing to validate) or when `redirect` is explicitly set in `fetchOptions`.
511
+
512
+ #### Blocking private IP addresses
513
+
514
+ The allowlist matches hostnames, not addresses, so an allowlisted host whose DNS record points at `127.0.0.1`, `169.254.169.254` or an RFC1918 address is fetched like any other. That is intended — allowlisted domains are trusted — but `blockPrivateIPs: true` adds a second line of defense for setups where the allowlist is broad (or `allowAllDomains` is on):
515
+
516
+ ```ts
517
+ ipxHttpStorage({ domains: ["cdn.example.com"], blockPrivateIPs: true });
518
+ ```
148
519
 
149
- You can universally customize IPX configuration using `IPX_*` environment variables.
520
+ The host of the requested URL **and of every redirect hop** must be a public address. IP literals are checked directly, hostnames are resolved with the OS resolver (`dns.lookup`, so `/etc/hosts` counts) and _all_ returned addresses must be public. Loopback, `0.0.0.0/8`, RFC1918, CGNAT (`100.64.0.0/10`), link-local (`169.254.0.0/16`, `fe80::/10`), unique-local (`fc00::/7`), multicast, reserved/test ranges and the IPv4-mapped/compatible IPv6 forms of all of them (`::ffff:127.0.0.1` and friends) are rejected with `403 IPX_FORBIDDEN_IP`. A host that cannot be resolved is a `502 IPX_DNS_LOOKUP_FAILED`, and a runtime without `node:net`/`node:dns` a `500 IPX_IP_CHECK_UNAVAILABLE` — the check fails closed rather than quietly turning itself off.
150
521
 
151
- - `IPX_ALIAS`
152
- - Default: `{}`
522
+ It is **off by default**: many deployments legitimately fetch from in-cluster origins (internal object storage, a sidecar, `localhost` in development), and enabling it by default would break them.
153
523
 
154
- ### Filesystem Source Options
524
+ Two limits to be aware of. It does not close the DNS-rebinding window: the name is resolved again when the socket is opened, so a record with a very short TTL can answer with a public address during validation and a private one during the fetch. Closing that requires pinning the validated address on the connection itself, which the plain `fetch` used here does not expose. And it is not a substitute for the allowlist — a public address that happens to be reachable is still fetched.
155
525
 
156
- (enabled by default with CLI only)
526
+ ## SVG Images
157
527
 
158
- #### `IPX_FS_DIR`
528
+ SVG images are not processed by sharp. They are sanitized, optimized with [svgo](https://github.com/svg/svgo) and served as `image/svg+xml`. Input that is not well-formed XML (an unescaped `&` is a common cause) is rejected with a `400 IPX_INVALID_SVG`.
159
529
 
160
- - Default: `.` (current working directory)
530
+ ```ts
531
+ createIPX({
532
+ storage,
533
+ svg: {
534
+ // SVGO config, or `false` to disable optimization
535
+ optimize: { multipass: true },
536
+ // Serve SVG images unsanitized. Only for fully trusted sources!
537
+ unsafeSkipSanitize: false,
538
+ },
539
+ });
540
+ ```
161
541
 
162
- #### `IPX_FS_MAX_AGE`
542
+ ### Optimization
543
+
544
+ SVGO's `preset-default` is applied unless you configure `plugins` yourself. Output is always a re-serialized document, never byte-identical to the source: ids are renamed, elements that are neither visible nor referenced within the same file are dropped, shapes are converted to paths and `<style>` rules are inlined.
545
+
546
+ That is fine for images used as `<img src>` or as CSS backgrounds, but it can break consumers that reach into the document:
547
+
548
+ - Sprite sheets: a `<symbol id="icon">` with no `<use>` in the same file is removed, so `<use href="/sprite.svg#icon">` renders nothing.
549
+ - References by id from outside the file, since ids are renamed (`icon-home` becomes `a`).
550
+ - Selectors in a host page that inlines the SVG, since `<rect>` becomes `<path>` and class-based rules are inlined.
551
+
552
+ Use `svg: { optimize: false }` to sanitize without optimizing, or keep optimization with the structural plugins disabled (about 1% larger output for typical icons):
553
+
554
+ ```ts
555
+ createIPX({
556
+ storage,
557
+ svg: {
558
+ optimize: {
559
+ plugins: [
560
+ {
561
+ name: "preset-default",
562
+ params: {
563
+ overrides: { cleanupIds: false, removeHiddenElems: false },
564
+ },
565
+ },
566
+ ],
567
+ },
568
+ },
569
+ });
570
+ ```
163
571
 
164
- - Default: `300`
572
+ ### Sanitization
165
573
 
166
- ### HTTP(s) Source Options
574
+ SVG documents can carry active content, so IPX **always** sanitizes them before serving. Sanitization is independent of optimization: `svg: { optimize: false }` only disables SVGO's optimization plugins.
167
575
 
168
- (enabled by default with CLI only)
576
+ Removed from every SVG:
169
577
 
170
- #### `IPX_HTTP_DOMAINS`
578
+ - `<script>` elements (including namespaced ones such as `<svg:script>`)
579
+ - Event handler attributes (any `on*` attribute)
580
+ - Embedded foreign documents: `<foreignObject>`, `<iframe>`, `<embed>`, `<object>`, `<base>`, `<link>` and `<meta>`
581
+ - Event handler elements: `<handler>` and `<listener>`
582
+ - SMIL animations (`<animate>`, `<animateMotion>`, `<animateTransform>` and `<set>`) that assign an `on*` attribute or an unsafe URI, which could otherwise re-introduce a handler after load
583
+ - URIs (`href`, `xlink:href` and `src`) with a scheme other than `http:`, `https:`, `mailto:`, `tel:`, `ftp:` or a non-SVG `data:image/*` — in particular `javascript:`, including obfuscated variants using entities or control characters
584
+ - The `<!DOCTYPE>` declaration and all processing instructions (`<?…?>`), which are serialized unescaped and can smuggle markup past an HTML parser
171
585
 
172
- - Default: `[]`
586
+ **External references are kept.** Attributes such as `<image href="https://…">`, `<use href="…">`, external fonts and `@import` inside `<style>` are preserved, since stripping them would break legitimate images. They are not a script execution vector, but they do allow the SVG to make requests to third-party origins (and thereby leak the viewer's IP address) when rendered as a document. If this matters for your threat model, host such images from a separate origin or block the requests with a Content-Security-Policy.
173
587
 
174
- #### `IPX_HTTP_MAX_AGE`
588
+ The bundled server sends `content-security-policy: default-src 'none'` with successful responses by default, which blocks both script execution and external references in browsers that honor it. Custom servers built on the programmatic API should send the same header, since sanitization cannot cover every future browser behavior on its own.
175
589
 
176
- - Default: `300`
590
+ Sanitization can be disabled with `svg: { unsafeSkipSanitize: true }`. Only do this when every source is fully trusted: IPX will then serve SVG images with XSS payloads unchanged.
177
591
 
178
- #### `IPX_HTTP_FETCH_OPTIONS`
592
+ ## Custom URL Style
179
593
 
180
- - Default: `{}`
594
+ The `parseURL` option accepts a function that extracts the resource id and modifiers from the request URL, allowing any URL style you like. It receives the raw (still percent-encoded) request URL, so it is free to decode it however the URL style requires.
181
595
 
182
- #### `IPX_HTTP_ALLOW_ALL_DOMAINS`
596
+ **Example:** modifiers in the filename (`/<id>@@<modifiers>.<format>`), which can be preferable when prerendering images for static hosting.
183
597
 
184
- - Default: `false`
598
+ ```ts
599
+ import { createIPXFetchHandler, parseIPXURL } from "ipx";
185
600
 
186
- ## Modifiers
601
+ const handler = createIPXFetchHandler(ipx, {
602
+ parseURL(url) {
603
+ const path = decodeURIComponent(new URL(url).pathname.slice(1));
187
604
 
188
- | Property | Docs | Example | Comments |
189
- | -------------- | :-------------------------------------------------------------- | :--------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190
- | width / w | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/width_200/buffalo.png` or `/w_200/buffalo.png` |
191
- | height / h | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/height_200/buffalo.png` or `/h_200/buffalo.png` |
192
- | resize / s | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/s_200x200/buffalo.png` |
193
- | kernel | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/s_200x200,kernel_nearest/buffalo.png` | Supported kernel: `nearest`, `cubic`, `mitchell`, `lanczos2` and `lanczos3` (default). |
194
- | fit | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/s_200x200,fit_outside/buffalo.png` | Sets `fit` option for `resize`. |
195
- | position / pos | [Docs](https://sharp.pixelplumbing.com/api-resize#resize) | `/s_200x200,pos_top/buffalo.png` | Sets `position` option for `resize`. |
196
- | trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `/trim_100/buffalo.png` |
197
- | extend | [Docs](https://sharp.pixelplumbing.com/api-resize#extend) | `/extend_{top}_{right}_{bottom}_{left}/buffalo.png` | Extend / pad / extrude one or more edges of the image with either the provided background colour or pixels derived from the image. |
198
- | background / b | \_ | `/r_45,b_00ff00/buffalo.png` |
199
- | extract | [Docs](https://sharp.pixelplumbing.com/api-resize#extract) | `/extract_{left}_{top}_{width}_{height}/buffalo.png` | Extract/crop a region of the image. |
200
- | crop | [Docs](https://sharp.pixelplumbing.com/api-resize#extract) | `/crop_{left}_{top}_{width}_{height}/buffalo.png` | Alias for extract. Extract/crop a region of the image. |
201
- | format / f | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `/format_webp/buffalo.png` or `/f_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif`, `tiff` and `auto` (experimental only with middleware) |
202
- | quality / q | \_ | `/quality_50/buffalo.png` or `/q_50/buffalo.png` | Accepted values: 0 to 100 |
203
- | rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `/rotate_45/buffalo.png` |
204
- | enlarge | \_ | `/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
205
- | flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `/flip/buffalo.png` |
206
- | flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `/flop/buffalo.png` |
207
- | sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `/sharpen_30/buffalo.png` |
208
- | median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `/median_10/buffalo.png` |
209
- | blur | [Docs](https://sharp.pixelplumbing.com/api-operation#blur) | `/blur_5/buffalo.png` |
210
- | gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `/gamma_3/buffalo.png` |
211
- | negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `/negate/buffalo.png` |
212
- | normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `/normalize/buffalo.png` |
213
- | threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `/threshold_10/buffalo.png` |
214
- | tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `/tint_1098123/buffalo.png` |
215
- | grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `/grayscale/buffalo.png` |
216
- | flatten | [Docs](https://sharp.pixelplumbing.com/api-operation#flatten) | `/flatten/buffalo.png` | Remove alpha channel, if any, and replace with background colour. |
217
- | modulate | [Docs](https://sharp.pixelplumbing.com/api-operation#modulate) | `/modulate_brightness_saturation_hue/buffalo.png` | Transforms the image using brightness, saturation and hue rotation. |
218
- | crop | [Docs](https://sharp.pixelplumbing.com/api-resize#extract) | `/crop_{left}_{top}_{width}_{height}/buffalo.png` | Alias for extract. Extract/crop a region of the image. |
219
- | animated / a | - | `/animated/buffalo.gif` or `/a/buffalo.gif` | Experimental |
605
+ const match = path.match(/^(.+)@@(.+)\.([^.]+)$/);
606
+ if (!match) {
607
+ // Not our style, fall back to the default `/<modifiers>/<id>`
608
+ return parseIPXURL(url);
609
+ }
610
+
611
+ const [, id = "", modifiersString = "", format = ""] = match;
612
+ const modifiers = Object.fromEntries(
613
+ modifiersString.split(",").map((m) => {
614
+ const [key = "", ...values] = m.split("_");
615
+ return [key, values.join("_")];
616
+ }),
617
+ );
618
+
619
+ return { id, modifiers: { ...modifiers, format } };
620
+ },
621
+ });
622
+
623
+ // http://localhost:3000/static/buffalo.png@@s_200x200.webp
624
+ // http://localhost:3000/static/buffalo.png@@grayscale,w_200.webp
625
+ ```
626
+
627
+ The parser may be async, and can throw an `HTTPError` (re-exported from `ipx`) to reject a request with a specific status code.
628
+
629
+ Returned values are escaped by IPX, so custom parsers don't need to do it themselves. Note this is not an access check — exactly as with the default URL style, what the resulting id is allowed to resolve to is enforced by the storage layer (`ipxFSStorage`'s directory boundary, `ipxHttpStorage`'s domain allowlist).
630
+
631
+ ## Migration from v3 to v4
632
+
633
+ - The server creation APIs have changed. See the [Programmatic API](#programmatic-api) section for examples.
634
+ - The JSON error format has changed from `{ error: string }` to `{ status, statusText, message }`.
635
+ - The `svgo` option is now `svg.optimize`. SVG images are always sanitized, even when optimization is disabled.
636
+ - SVG optimization now applies SVGO's `preset-default` unless custom `plugins` are configured (previously only the configured plugins ran), so SVG output is restructured more than before. See the [SVG Images](#svg-images) section.
220
637
 
221
638
  ## License
222
639