# truss [![Build](https://github.com/nao1215/truss/actions/workflows/rust.yml/badge.svg)](https://github.com/nao1215/truss/actions/workflows/rust.yml) [![CLI Integration](https://github.com/nao1215/truss/actions/workflows/integration-cli.yml/badge.svg)](https://github.com/nao1215/truss/actions/workflows/integration-cli.yml) [![API Integration](https://github.com/nao1215/truss/actions/workflows/integration.yml/badge.svg)](https://github.com/nao1215/truss/actions/workflows/integration.yml) [![Crates.io](https://img.shields.io/crates/v/truss-image)](https://crates.io/crates/truss-image) [![Crates.io Downloads](https://img.shields.io/crates/d/truss-image)](https://crates.io/crates/truss-image) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Rust](https://img.shields.io/badge/rust-stable-orange)](https://www.rust-lang.org/) ![logo](./docs/img/logo-small.png) truss is an image transformation tool with a shared Rust core for a CLI, an HTTP server, and a browser/WASM build. Use the CLI for local files and shell pipelines, run the server behind a CDN or reverse proxy, process files in the browser with [`@nao1215/truss-wasm`](https://www.npmjs.com/package/@nao1215/truss-wasm), or sign public URLs from Node.js with [`@nao1215/truss-url-signer`](./packages/truss-url-signer). [Try the WASM demo in your browser](https://nao1215.github.io/truss/) - no install, no upload, runs 100 % client-side. ![WASM demo screenshot](./docs/img/wasm-github-pages.png) ## Start Here | If you want to... | Start with | Read next | |-------------------|------------|-----------| | Convert local files from the shell | `brew install nao1215/tap/truss` or `cargo install truss-image`, then `truss photo.png -o photo.jpg` | [CLI](#cli), [Commands](#commands) | | Run an HTTP image server | `TRUSS_BEARER_TOKEN=changeme truss serve --bind 0.0.0.0:8080 --storage-root ./images` | [HTTP Server](#http-server), [API Reference](docs/api-reference.md), [Deployment Guide](docs/deployment.md) | | Process files in a browser app | `npm install @nao1215/truss-wasm` | [WASM](#wasm), [WASM Integration](docs/wasm.md) | | Generate signed public URLs from Node.js | `npm install @nao1215/truss-url-signer` | [TypeScript URL Signing](#typescript-url-signing), [Signed URL Specification](docs/signed-url-spec.md) | ## Why truss? - One Rust core across the CLI, the HTTP server, and the browser/WASM build. - Signed URLs, SSRF protections, and SVG sanitization are built in. - Supports JPEG, PNG, WebP, AVIF, BMP, TIFF, and SVG. - Runs on Linux, macOS, and Windows. - CLI behavior is covered by [ShellSpec](https://github.com/shellspec/shellspec), and the HTTP API by [runn](https://github.com/k1LoW/runn). ## Installation ### CLI #### Homebrew ```sh brew install nao1215/tap/truss ``` #### Cargo ```sh cargo install truss-image ``` Need S3, GCS, or Azure storage backend support at install time? See the [Deployment Guide](docs/deployment.md#installing-with-storage-backend-support). Prebuilt binaries are available on the [GitHub Releases](https://github.com/nao1215/truss/releases) page. See [Deployment Guide](docs/deployment.md) for all targets and Docker images. ### JavaScript Packages Install only the package you need: ```sh # Browser/WASM package npm install @nao1215/truss-wasm # Node.js URL signer npm install @nao1215/truss-url-signer ``` Package source and package-specific READMEs live in [`packages/truss-wasm`](./packages/truss-wasm) and [`packages/truss-url-signer`](./packages/truss-url-signer). ### Container Image ```sh docker pull ghcr.io/nao1215/truss:latest ``` ## Quick Start ### CLI The `convert` subcommand can be omitted: `truss photo.png -o photo.jpg` is equivalent to `truss convert photo.png -o photo.jpg`. Run `truss convert --help` to see the full set of options. ```sh # Convert format truss photo.png -o photo.jpg # Resize + convert truss photo.png -o thumb.webp --width 800 --format webp --quality 75 # Optimize in place with the shared pipeline truss optimize photo.jpg -o photo-optimized.jpg --mode auto # Convert from a remote URL truss --url https://example.com/img.png -o out.avif --format avif # Sanitize SVG (remove scripts and external references) truss diagram.svg -o safe.svg # Inspect metadata truss inspect photo.jpg ``` #### Format conversion & quality truss supports **JPEG, PNG, WebP, AVIF, BMP, TIFF, and SVG**. The output format is inferred from the file extension, or you can specify it explicitly with `--format`. | Format | File size (640 x 427) | Notes | |--------|----------------------:|-------| | JPEG (original) | 80 KB | Lossy, widely supported | | WebP (`--quality 80`) | 38 KB | ~52 % smaller than JPEG | | AVIF (`--quality 50`) | 17 KB | ~79 % smaller than JPEG | | PNG | 480 KB | Lossless | ```sh # JPEG -> WebP (smaller file, same visual quality) truss photo.jpg -o photo.webp --quality 80 # JPEG -> AVIF (best compression) truss photo.jpg -o photo.avif --quality 50 # Explicit format override (ignore extension) truss photo.jpg -o output.bin --format png ``` Use `--quality <1-100>` to control lossy encoding. Lower values produce smaller files at the cost of visual quality. Use `--optimize auto|lossless|lossy` on `truss convert`, or the dedicated `truss optimize` subcommand, to reduce output size with format-aware encoding choices. Add `--target-quality ssim:0.98` or `--target-quality psnr:42` when you want lossy optimization to aim for a specific perceptual threshold. | Quality 90 (95 KB) | Original (80 KB) | Quality 30 (27 KB) | |---|---|---| | ![q90](./docs/img/sample-bee-q90.jpg) | ![original](./docs/img/sample-bee.jpg) | ![q30](./docs/img/sample-bee-q30.jpg) | #### Resize & fit modes Specify `--width` and/or `--height` to resize. When both are given, `--fit` controls how the image fits the target box: | Mode | Behavior | |------|----------| | `contain` (default) | Scale down to fit entirely inside the box, preserving aspect ratio. Padding is filled with `--background`. | | `cover` | Scale to fill the box completely, cropping excess. Use `--position` to choose the crop anchor. | | `fill` | Stretch to exact dimensions (ignores aspect ratio). | | `inside` | Like `contain`, but never upscales a smaller image. | | Original (640 x 427) | contain 300 x 300 | cover 300 x 300 | fill 300 x 300 | inside 300 x 300 | |---|---|---|---|---| | ![original](./docs/img/sample-bee.jpg) | ![contain](./docs/img/sample-bee-contain.jpg) | ![cover](./docs/img/sample-bee-cover.jpg) | ![fill](./docs/img/sample-bee-fill.jpg) | ![inside](./docs/img/sample-bee-inside.jpg) | ```sh # contain -- fit inside the box, pad with gray background truss photo.jpg -o out.jpg --width 300 --height 300 --fit contain --background CCCCCC # cover -- fill the box, crop the excess truss photo.jpg -o out.jpg --width 300 --height 300 --fit cover # fill -- stretch to exact dimensions truss photo.jpg -o out.jpg --width 300 --height 300 --fit fill # inside -- like contain, but never upscale truss photo.jpg -o out.jpg --width 300 --height 300 --fit inside # Width only -- height is calculated to preserve aspect ratio truss photo.jpg -o out.jpg --width 800 ``` #### Cover position When using `--fit cover`, `--position` controls which part of the image is kept: | `--position top-left` | `--position center` (default) | `--position bottom-right` | |---|---|---| | ![top-left](./docs/img/sample-bee-cover-topleft.jpg) | ![center](./docs/img/sample-bee-cover.jpg) | ![bottom-right](./docs/img/sample-bee-cover-bottomright.jpg) | Available positions: `center`, `top`, `right`, `bottom`, `left`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. ```sh truss photo.jpg -o thumb.jpg --width 300 --height 300 --fit cover --position top-left ``` #### Crop, rotate & background ```sh # Crop a region (x, y, width, height) -- applied before resize truss photo.jpg -o cropped.jpg --crop 100,50,400,300 # Rotate 270 degrees clockwise (accepts 0, 90, 180, 270) truss photo.jpg -o rotated.jpg --rotate 270 # Background color as RRGGBB or RRGGBBAA hex (useful with contain or PNG alpha) truss photo.jpg -o out.png --width 300 --height 300 --fit contain --background FF6B35FF ``` | Original | Crop (`--crop 100,50,400,300`) | Rotate (`--rotate 270`) | Background (`--background FF6B35FF`) | |---|---|---|---| | ![original](./docs/img/sample-bee.jpg) | ![cropped](./docs/img/sample-bee-cropped.jpg) | ![rotated](./docs/img/sample-bee-rotated.jpg) | ![background](./docs/img/sample-bee-bg.png) | #### Blur, sharpen & watermark | Original | Gaussian Blur (`--blur 5.0`) | Sharpen (`--sharpen 3.0`) | Watermark | |---|---|---|---| | ![original](./docs/img/sample-bee.jpg) | ![blurred](./docs/img/sample-bee-blurred.jpg) | ![sharpened](./docs/img/sample-bee-sharpened.jpg) | ![watermarked](./docs/img/sample-bee-watermarked.jpg) | ```sh # Gaussian blur (sigma 0.1 - 100.0) truss photo.jpg -o blurred.jpg --blur 5.0 # Sharpen (sigma 0.1 - 100.0) truss photo.jpg -o sharpened.jpg --sharpen 3.0 # Watermark with full control truss photo.jpg -o watermarked.jpg \ --watermark logo.png \ --watermark-position bottom-right \ --watermark-opacity 50 \ --watermark-margin 10 ``` Watermark positions are the same as cover positions: `center`, `top`, `right`, `bottom`, `left`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. > **Note:** `--blur`, `--sharpen`, and `--watermark` are raster-only and not supported for SVG inputs. #### Metadata control By default, truss strips all metadata for smaller and safer output. | Flag | Behavior | |------|----------| | `--strip-metadata` (default) | Remove all EXIF, ICC, and other metadata | | `--keep-metadata` | Preserve EXIF, ICC, and all other supported metadata | | `--preserve-exif` | Keep EXIF only, strip ICC and others | | `--auto-orient` (default) | Apply EXIF orientation tag and reset it | | `--no-auto-orient` | Skip EXIF orientation correction | ```sh # Keep all metadata (useful for archival) truss photo.jpg -o out.jpg --keep-metadata # Keep EXIF only (strip ICC profiles) truss photo.jpg -o out.jpg --preserve-exif # Disable auto-orientation truss photo.jpg -o out.jpg --no-auto-orient ``` #### Stdin / stdout piping Use `-` for input and/or output to integrate truss into shell pipelines. When reading from stdin, `--format` is required for output. ```sh # Pipe from stdin to stdout cat photo.png | truss - -o - --format jpeg > photo.jpg # Download, convert, and upload in one pipeline curl -s https://example.com/img.png | truss - -o - --format webp --width 800 | \ aws s3 cp - s3://bucket/thumb.webp # Optimize after converting truss photo.jpg -o - --format webp --optimize auto | cat > optimized.webp ``` #### SVG handling truss sanitizes SVG files by removing scripts and external references, making them safe for user-generated content. ```sh # Sanitize SVG (remove scripts, external refs) truss diagram.svg -o safe.svg # Rasterize SVG to PNG at a specific width truss diagram.svg -o diagram.png --width 1024 ``` #### Filenames starting with `-` Use `--output=` (or `--output`) to avoid ambiguity with filenames that start with a dash: ```sh # Dash-prefixed output: use --output= to assign the value unambiguously truss convert input.png --output=-output.jpg # Dash-prefixed input: use a path prefix truss convert ./-input.png -o out.jpg ``` ### HTTP Server Start the server from an installed binary or a local build: ```sh TRUSS_BEARER_TOKEN=changeme truss serve --bind 0.0.0.0:8080 --storage-root ./images ``` Or run the published container image: ```sh docker run -p 8080:8080 \ -e TRUSS_BIND_ADDR=0.0.0.0:8080 \ -e TRUSS_BEARER_TOKEN=changeme \ -e TRUSS_STORAGE_ROOT=/data \ -v "$(pwd)/images:/data:ro" \ ghcr.io/nao1215/truss:latest ``` Resize a local image to 400 px wide WebP in one request: ```sh curl -X POST http://localhost:8080/images \ -H "Authorization: Bearer changeme" \ -F "file=@photo.jpg" \ -F 'options={"format":"webp","width":400}' \ -o thumb.webp ``` If you also want public signed URLs, start the server with signing keys and generate URLs with `truss sign`: ```sh TRUSS_BEARER_TOKEN=changeme \ TRUSS_SIGNING_KEYS='{"mykey":"s3cret"}' \ truss serve --bind 0.0.0.0:8080 --storage-root ./images truss sign --base-url http://localhost:8080 \ --path photos/hero.jpg \ --key-id mykey \ --secret s3cret \ --expires 1900000000 \ --width 800 \ --format webp ``` Use `truss validate` to check server configuration without starting the process. See the [API Reference](docs/api-reference.md) for endpoint details and the [Deployment Guide](docs/deployment.md) for Docker, storage backends, and production setup. ### TypeScript URL Signing Node.js only. The signer uses `node:crypto` and should stay on the server side; do not ship the signing secret to browsers or Edge/browser runtimes. ```sh npm install @nao1215/truss-url-signer ``` ```ts import { signPublicUrl } from "@nao1215/truss-url-signer"; const signedUrl = signPublicUrl({ baseUrl: "https://images.example.com", source: { kind: "path", path: "hero.jpg", }, transforms: { width: 1200, format: "webp", }, keyId: "public-demo", secret: process.env.TRUSS_SIGNING_SECRET ?? "", expires: Math.floor(Date.now() / 1000) + 300, }); ``` See [`packages/truss-url-signer`](./packages/truss-url-signer) for the full API and examples for both `/images/by-path` and `/images/by-url`. ### WASM truss also ships a browser-oriented WASM adapter for local, client-side image processing. The generated package exposes a small JS-facing API over the same Rust core used by the CLI and HTTP server. For bundler-based browser apps, the repository includes the source for the official npm package in [`packages/truss-wasm`](./packages/truss-wasm). It uses the fixed feature set `wasm,svg,avif`, so AVIF is available and WebP stays lossless in the package build. ```sh npm install @nao1215/truss-wasm ``` If you want a minimal consumer you can run immediately, see [`examples/vite-truss-wasm`](./examples/vite-truss-wasm). ```js import { getCapabilitiesJson, inspectImageJson, transformImage, } from "@nao1215/truss-wasm"; const inputBytes = new Uint8Array(await file.arrayBuffer()); const capabilities = JSON.parse(getCapabilitiesJson()); const inspected = JSON.parse(inspectImageJson(inputBytes, undefined)); const result = transformImage( inputBytes, undefined, JSON.stringify({ format: "jpeg", width: 1200, quality: 80, }), ); ``` The official npm package wraps the raw browser bindings and initializes the Wasm module at import time, so consumer code does not call `init()` explicitly. For maintainers: - `node ./scripts/run-wasm-consumer-smoke.mjs` packs the local npm artifact, installs it into a throwaway consumer, and runs one real transform through the published JS surface. - `node ./scripts/run-wasm-vite-example-smoke.mjs` rewires the Vite example to the local tarball and verifies that a real bundler build still succeeds. - `node ./scripts/run-wasm-vite-example-runtime-smoke.mjs` verifies the checked-in Vite example and confirms the browser runtime path in headless Chrome. The example below assumes your page is served from a directory that also contains `pkg/truss.js`. When using `./scripts/build-wasm-demo.sh`, that means `web/dist/index.html` importing `./pkg/truss.js`. ```js import init, { getCapabilitiesJson, inspectImageJson, transformImage, } from "./pkg/truss.js"; await init(); const inputBytes = new Uint8Array(await file.arrayBuffer()); const capabilities = JSON.parse(getCapabilitiesJson()); const inspected = JSON.parse(inspectImageJson(inputBytes, undefined)); const result = transformImage( inputBytes, undefined, JSON.stringify({ format: "jpeg", width: 1200, quality: 80, }), ); const response = JSON.parse(result.responseJson); const outputBlob = new Blob([result.bytes], { type: response.artifact.mimeType, }); ``` The GitHub Pages demo is intentionally built with `wasm,svg`. The official npm package uses `wasm,svg,avif`. Check capabilities at runtime and see [WASM Integration](docs/wasm.md) for package and raw-build usage, feature differences, import-path assumptions, API shapes, constraints, limits, and error handling. ## Commands | Command | Description | |---------|-------------| | `convert` | Convert and transform an image file (can be omitted; see above) | | `optimize` | Optimize an image with format-aware auto/lossless/lossy modes (`truss optimize photo.jpg -o photo-optimized.jpg --mode auto`) | | `inspect` | Show metadata (format, dimensions, alpha) of an image | | `serve` | Start the HTTP image-transform server (implied when server flags are used at the top level) | | `validate` | Validate server configuration without starting the server (useful in CI/CD) | | `sign` | Generate a signed public URL for the server | | `completions` | Generate shell completion scripts | | `help` | Show help for a command (for example `truss help convert`) | Top-level shortcuts: - `truss -o [OPTIONS]` is implicit `convert` - `truss --bind [OPTIONS]` is implicit `serve` - `truss --version` prints version information ## Documentation | Page | Description | |------|-------------| | [Configuration Reference](docs/configuration.md) | Environment variables, storage backends, logging, and all server settings | | [API Reference](docs/api-reference.md) | HTTP endpoints, request/response formats, CDN integration | | [Signed URL Specification](docs/signed-url-spec.md) | Canonicalization rules, compatibility policy, and SDK guidance for public signed URLs | | [Deployment Guide](docs/deployment.md) | Docker, prebuilt binaries, cloud storage (S3/GCS/Azure), production setup | | [Development Guide](docs/development.md) | Building from source, testing, benchmarks, WASM demo, contributing | | [WASM Integration](docs/wasm.md) | Browser build flags, JS API contract, runtime capabilities, limits, and caveats | | [Prometheus Metrics](docs/prometheus.md) | Metrics reference, bucket boundaries, example PromQL queries | | [Pipeline Order](docs/pipeline.md) | Transform stage order and SVG-specific constraints | | [OpenAPI Spec](docs/openapi.yaml) | Machine-readable API specification | ## Architecture ```mermaid flowchart TB CLI["CLI
(truss convert)"] --> Core Server["HTTP Server
(truss serve)"] --> Core WASM["WASM
(browser)"] --> Core subgraph Core["Shared Rust core"] direction LR Sniff["Detect format"] --> Transform["Crop / resize / blur / sharpen / watermark"] Transform --> Encode["Encode output"] end Server --> Storage subgraph Storage["Storage backends"] FS["Local filesystem"] S3["S3"] GCS["GCS"] Azure["Azure Blob"] end ``` CLI reads local files or fetches remote URLs directly. The HTTP server resolves images from storage backends or client uploads. The WASM build processes files selected in the browser. ## Performance Benchmarks are defined in [`benches/transform.rs`](./benches/transform.rs). The numbers below were measured with [criterion](https://github.com/bheisler/criterion.rs) on a single core using the sample image in this repository. Use them as a local reference and run `just bench` on your hardware to compare. | Operation | Time | |-----------|-----:| | JPEG -> PNG | 8.2 us | | JPEG -> WebP (q 80) | 37 us | | JPEG -> AVIF (q 80) | 242 us | | Resize 100 x 100 (cover) | 317 us | | Resize 800 x 600 (cover) | 11.4 ms | | Resize 1920 x 1080 (cover) | 68 ms | | Blur (sigma 5.0) | 6.8 us | | Sharpen (sigma 3.0) | 5.9 us | | SVG sanitize (passthrough) | 386 ns | | SVG -> PNG 1024 w rasterize | 649 us | | Format detection (`sniff`) | 18 ns | ## Comparison Feature comparison with [imgproxy](https://github.com/imgproxy/imgproxy) and [imagor](https://github.com/cshum/imagor) as of March 2026. Check upstream documentation if you need to confirm a specific feature before migrating or standardizing on a tool. | Feature | truss | imgproxy | imagor | |---------|:-----:|:--------:|:------:| | Language | Rust | Go | Go | | Runtime dependencies | None | libvips (C) | libvips (C) | | CLI | Yes | No | No | | WASM browser demo | Yes | No | No | | Signed URLs | Yes | Yes | Yes | | JPEG / PNG / WebP / AVIF | Yes | Yes | Yes | | JPEG XL (JXL) | No | Input only | Yes | | TIFF | Yes | Yes | Yes | | GIF animation processing | No (out of scope) | Yes | Yes | | SVG sanitization | Yes | Yes | No | | Smart crop | No | Yes | Yes | | Sharpen filter | Yes | Yes | Yes | | Crop / Trim / Padding | Yes | Yes | Yes | | S3 | Yes | Yes | Yes | | GCS | Yes | Yes | Yes | | Azure Blob Storage | Yes | Yes | No | | Watermark | Yes | Yes | Yes | | Prometheus metrics | Yes | Yes | Yes | | License | MIT | Apache 2.0 | Apache 2.0 | ## Roadmap See the [public roadmap](https://github.com/nao1215/truss/issues?q=is%3Aissue+label%3Aroadmap) for planned features and milestones. ## Contributing Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for details. - Look for [`good first issue`](https://github.com/nao1215/truss/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to get started. - Report bugs and request features via [Issues](https://github.com/nao1215/truss/issues). - If the project is useful, starring the repository helps. - Support via [GitHub Sponsors](https://github.com/sponsors/nao1215) is also welcome. - Sharing the project on social media or in blog posts is appreciated. ## License Released under the [MIT License](LICENSE).