Skip to content

unjs/ipx

Repository files navigation

🖼️ IPX

npm version npm downloads

High performance, secure and easy-to-use image optimizer powered by sharp and svgo.

Used by Nuxt Image and Netlify and open to everyone!

Migration from v3 to v4

Note

This is the active development branch for IPX v4. Check out v3 for v3 docs.

  • The server creation APIs have changed. See the Programmatic API section for examples.
  • The JSON error format has changed from { error: string } to { status, statusText, message }.
  • The svgo option is now svg.optimize. SVG images are always sanitized, even when optimization is disabled.
  • 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 section.

Using CLI

You can use ipx command to start server.

Using npx:

npx ipx serve --dir ./

Using bun

bunx ipx serve --dir ./

The default serve directory is the current working directory.

Programmatic API

You can use IPX as a middleware or directly use IPX interface.

Example: Using built-in server

import { serveIPX, createIPX, ipxFSStorage, ipxHttpStorage } from "ipx";

const ipx = createIPX({
  storage: ipxFSStorage({ dir: "./public" }),
  httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
  alias: { "/picsum": "https://picsum.photos" },
});

// http://localhost:3000/w_512/picsum/1000
serveIPX(ipx);

Example: Using with h3

import { H3, serve } from "h3";

import {
  createIPX,
  ipxFSStorage,
  ipxHttpStorage,
  createIPXFetchHandler,
} from "ipx";

const ipx = createIPX({
  storage: ipxFSStorage({ dir: "./public" }),
  httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
  alias: { "/picsum": "https://picsum.photos" },
});

const app = new H3();

app.mount("/ipx", createIPXFetchHandler(ipx));

// http://localhost:3000/ipx/w_512/picsum/1000
serve(app);

Example: Using with express

import Express from "express";

import {
  createIPX,
  ipxFSStorage,
  ipxHttpStorage,
  createIPXNodeHandler,
} from "ipx";

import type { RequestHandler } from "express";

const ipx = createIPX({
  storage: ipxFSStorage({ dir: "./public" }),
  httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
  alias: { "/picsum": "https://picsum.photos" },
});

const app = Express();

app.use("/ipx", createIPXNodeHandler(ipx) as RequestHandler);

// http://localhost:3000/ipx/w_512/picsum/1000
app.listen(3000, () => {
  console.log("Server is running on http://localhost:3000");
});

URL Examples

Get original image:

/_/static/buffalo.png

Change format to webp and keep other things same as source:

/f_webp/static/buffalo.png

Automatically convert to a preferred format (avif/webp/jpeg). Uses the browsers accept header to negotiate:

/f_auto/static/buffalo.png

Keep original format (png) and set width to 200:

/w_200/static/buffalo.png

Resize to 200x200px using embed method and change format to webp:

/embed,f_webp,s_200x200/static/buffalo.png

Custom URL Style

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.

Example: modifiers in the filename (/<id>@@<modifiers>.<format>), which can be preferable when prerendering images for static hosting.

import { createIPXFetchHandler, parseIPXURL } from "ipx";

const handler = createIPXFetchHandler(ipx, {
  parseURL(url) {
    const path = decodeURIComponent(new URL(url).pathname.slice(1));

    const match = path.match(/^(.+)@@(.+)\.([^.]+)$/);
    if (!match) {
      // Not our style, fall back to the default `/<modifiers>/<id>`
      return parseIPXURL(url);
    }

    const [, id = "", modifiersString = "", format = ""] = match;
    const modifiers = Object.fromEntries(
      modifiersString.split(",").map((m) => {
        const [key = "", ...values] = m.split("_");
        return [key, values.join("_")];
      }),
    );

    return { id, modifiers: { ...modifiers, format } };
  },
});

// http://localhost:3000/static/buffalo.png@@s_200x200.webp
// http://localhost:3000/static/buffalo.png@@grayscale,w_200.webp

The parser may be async, and can throw an HTTPError (re-exported from ipx) to reject a request with a specific status code.

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).

Config

You can universally customize IPX configuration using IPX_* environment variables.

  • IPX_ALIAS
    • Default: {}
  • IPX_MAX_OUTPUT_DIMENSION
    • Default: 8192
    • Maximum width and height (in pixels) of the output image (option: maxOutputDimension). Requested width, height and resize dimensions are clamped to it, 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.

Filesystem Source Options

(enabled by default with CLI only)

IPX_FS_DIR

  • Default: . (current working directory)

IPX_FS_MAX_AGE

  • Default: 300

HTTP(s) Source Options

(enabled by default with CLI only)

IPX_HTTP_DOMAINS

  • Default: []

IPX_HTTP_MAX_AGE

  • Default: 300

IPX_HTTP_FETCH_OPTIONS

  • Default: {}

IPX_HTTP_ALLOW_ALL_DOMAINS

  • Default: false

Modifiers

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. Trailing arguments may be omitted to keep the sharp default, except where noted.

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).

Property Docs Example Comments
width / w Docs /width_200/buffalo.png or /w_200/buffalo.png Positive integer.
height / h Docs /height_200/buffalo.png or /h_200/buffalo.png Positive integer.
resize / s Docs /s_200x200/buffalo.png {width}x{height} of positive integers. A single value (/s_200/) is a square.
kernel Docs /s_200x200,kernel_nearest/buffalo.png Sets kernel option for resize. One of nearest, linear, cubic, mitchell, lanczos2, lanczos3 (default), mks2013 or mks2021.
fit Docs /s_200x200,fit_outside/buffalo.png Sets fit option for resize. One of contain, cover (default), fill, inside or outside.
position / pos Docs /s_200x200,pos_top/buffalo.png Sets position option for resize. A position (top, right top, right, right bottom, bottom, left bottom, left, left top), gravity (north, northeast, ..., center) or strategy (entropy, attention). Since _ separates arguments, multi-word values use - (/pos_right-top/). The numeric gravity (0-8) and strategy (16-17) constants are also accepted.
trim Docs /trim_100/buffalo.png Trim threshold, a number >= 0 (defaults to 10).
extend Docs /extend_10_20_10_20_mirror/buffalo.png Extend / pad / extrude one or more edges of the image. Format: /extend_{top}_{right}_{bottom}_{left}_{extendWith}/. Edges are integers between 0 and 10000. Optional extendWith: background (default), copy, repeat or mirror. When extending with background, the colour comes from the background / b modifier, e.g. /b_00ff00,extend_10_20_10_20/buffalo.png.
background / b _ /r_45,b_00ff00/buffalo.png Background colour used by extend, rotate, flatten and resize (with fit_contain).
extract Docs /extract_{left}_{top}_{width}_{height}/buffalo.png Extract/crop a region of the image. All four arguments are required; width and height must be positive.
crop Docs /crop_{left}_{top}_{width}_{height}/buffalo.png Alias for extract. Extract/crop a region of the image.
format / f Docs /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)
quality / q _ /quality_50/buffalo.png or /q_50/buffalo.png Integer between 1 and 100.
rotate Docs /rotate_45/buffalo.png Angle in degrees, between -3600 and 3600. Angles that are not a multiple of 90 are filled with the background / b colour. Without an angle (/rotate/) the image is auto-oriented from its EXIF tag.
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.
flip Docs /flip/buffalo.png
flop Docs /flop/buffalo.png
sharpen Docs /sharpen_2_1_2/buffalo.png {sigma}_{flat}_{jagged}. sigma is between 0.000001 and 10, flat and jagged between 0 and 1000000. Without arguments (/sharpen/) a mild sharpen is applied.
median Docs /median_10/buffalo.png Square mask size, an integer between 1 and 1000 (defaults to 3).
blur Docs /blur_5/buffalo.png Sigma, a number between 0.3 and 1000. Without an argument (/blur/) a mild blur is applied.
unflatten Docs /unflatten/buffalo.png
gamma Docs /gamma_2.2_1.8/buffalo.png {gamma}_{gammaOut}, each a number between 1.0 and 3.0 (defaults to 2.2).
negate Docs /negate/buffalo.png
normalize Docs /normalize/buffalo.png
threshold Docs /threshold_10/buffalo.png Integer between 0 and 255 (defaults to 128).
tint Docs /tint_00ff00/buffalo.png Tint colour.
grayscale Docs /grayscale/buffalo.png
flatten Docs /flatten/buffalo.png Remove alpha channel, if any, and replace with the background / b colour.
modulate Docs /modulate_2_1.2_90_10/buffalo.png Transforms the image using {brightness}_{saturation}_{hue}_{lightness}. brightness and saturation are numbers >= 0, hue is an integer in degrees.
animated / a - /animated/buffalo.gif or /a/buffalo.gif Experimental

SVG Images

SVG images are not processed by sharp. They are sanitized, optimized with 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.

createIPX({
  storage,
  svg: {
    // SVGO config, or `false` to disable optimization
    optimize: { multipass: true },
    // Serve SVG images unsanitized. Only for fully trusted sources!
    unsafeSkipSanitize: false,
  },
});

Optimization

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.

That is fine for images used as <img src> or as CSS backgrounds, but it can break consumers that reach into the document:

  • Sprite sheets: a <symbol id="icon"> with no <use> in the same file is removed, so <use href="/sprite.svg#icon"> renders nothing.
  • References by id from outside the file, since ids are renamed (icon-home becomes a).
  • Selectors in a host page that inlines the SVG, since <rect> becomes <path> and class-based rules are inlined.

Use svg: { optimize: false } to sanitize without optimizing, or keep optimization with the structural plugins disabled (about 1% larger output for typical icons):

createIPX({
  storage,
  svg: {
    optimize: {
      plugins: [
        {
          name: "preset-default",
          params: {
            overrides: { cleanupIds: false, removeHiddenElems: false },
          },
        },
      ],
    },
  },
});

Sanitization

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.

Removed from every SVG:

  • <script> elements (including namespaced ones such as <svg:script>)
  • Event handler attributes (any on* attribute)
  • Embedded foreign documents: <foreignObject>, <iframe>, <embed>, <object>, <base>, <link> and <meta>
  • Event handler elements: <handler> and <listener>
  • 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
  • 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
  • The <!DOCTYPE> declaration and all processing instructions (<?…?>), which are serialized unescaped and can smuggle markup past an HTML parser

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.

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.

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.

License

MIT

About

🖼️ High performance, secure and easy-to-use image optimizer.

Topics

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages