Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
run: pip install openapi-spec-validator

- name: Validate OpenAPI spec
run: python -m openapi_spec_validator doc/openapi.yaml
run: python -m openapi_spec_validator docs/openapi.yaml

# ── security audit ──────────────────────────────────────────────────
security-audit:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
### Added

- Prometheus `/metrics` endpoint with histograms (HTTP request duration, transform duration, storage duration) and error counters.
- Prometheus metrics documentation (`doc/prometheus.md`).
- Prometheus metrics documentation (`docs/prometheus.md`).
- Dedicated 304 status counter for cache-validation traffic tracking.

### Changed
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ integration/
├── compose.yml
└── runbooks/*.yml

doc/ # Design documents and specs
docs/ # Design documents, API specs, and guides
```

### Architecture overview
Expand Down Expand Up @@ -165,7 +165,7 @@ truss follows a three-layer architecture:
3. Implement the operation in `transform_raster()` in `codecs/raster.rs`
4. Add CLI flag parsing in `cli/convert.rs`
5. Add HTTP query parameter parsing in `server/http_parse.rs`
6. Update the OpenAPI spec in `doc/openapi.yaml`
6. Update the OpenAPI spec in `docs/openapi.yaml`

**How to add a new storage backend:**

Expand Down
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
[![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](./doc/img/logo-small.png)
![logo](./docs/img/logo-small.png)

Resize, crop, convert, blur, sharpen, and watermark images from the CLI, an HTTP server, or the browser -- written in Rust with signed-URL authentication and SSRF protection built in.
Resize, crop, convert, optimize, blur, sharpen, and watermark images from the CLI, an HTTP server, or the browser -- written in Rust with signed-URL authentication and SSRF protection built in.

[Try the WASM demo in your browser](https://nao1215.github.io/truss/) -- no install, no upload, runs 100 % client-side.

![WASM demo screenshot](./doc/img/wasm-sample.png)
![WASM demo screenshot](./docs/img/wasm-sample.png)

## Why truss?

Expand Down Expand Up @@ -97,6 +97,9 @@ 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

Expand Down Expand Up @@ -131,9 +134,11 @@ 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](./doc/img/sample-bee-q90.jpg) | ![original](./doc/img/sample-bee.jpg) | ![q30](./doc/img/sample-bee-q30.jpg) |
| ![q90](./docs/img/sample-bee-q90.jpg) | ![original](./docs/img/sample-bee.jpg) | ![q30](./docs/img/sample-bee-q30.jpg) |

#### Resize & fit modes

Expand All @@ -148,7 +153,7 @@ Specify `--width` and/or `--height` to resize. When both are given, `--fit` cont

| Original (640 × 427) | contain 300 × 300 | cover 300 × 300 | fill 300 × 300 | inside 300 × 300 |
|---|---|---|---|---|
| ![original](./doc/img/sample-bee.jpg) | ![contain](./doc/img/sample-bee-contain.jpg) | ![cover](./doc/img/sample-bee-cover.jpg) | ![fill](./doc/img/sample-bee-fill.jpg) | ![inside](./doc/img/sample-bee-inside.jpg) |
| ![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
Expand All @@ -173,7 +178,7 @@ When using `--fit cover`, `--position` controls which part of the image is kept:

| `--position top-left` | `--position center` (default) | `--position bottom-right` |
|---|---|---|
| ![top-left](./doc/img/sample-bee-cover-topleft.jpg) | ![center](./doc/img/sample-bee-cover.jpg) | ![bottom-right](./doc/img/sample-bee-cover-bottomright.jpg) |
| ![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`.

Expand All @@ -196,13 +201,13 @@ truss photo.jpg -o out.png --width 300 --height 300 --fit contain --background F

| Original | Crop (`--crop 100,50,400,300`) | Rotate (`--rotate 270`) | Background (`--background FF6B35FF`) |
|---|---|---|---|
| ![original](./doc/img/sample-bee.jpg) | ![cropped](./doc/img/sample-bee-cropped.jpg) | ![rotated](./doc/img/sample-bee-rotated.jpg) | ![background](./doc/img/sample-bee-bg.png) |
| ![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](./doc/img/sample-bee.jpg) | ![blurred](./doc/img/sample-bee-blurred.jpg) | ![sharpened](./doc/img/sample-bee-sharpened.jpg) | ![watermarked](./doc/img/sample-bee-watermarked.jpg) |
| ![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)
Expand Down Expand Up @@ -258,8 +263,8 @@ cat photo.png | truss - -o - --format jpeg > photo.jpg
curl -s https://example.com/img.png | truss - -o - --format webp --width 800 | \
aws s3 cp - s3://bucket/thumb.webp

# Combine with other tools
truss photo.jpg -o - --format png --width 400 | pngquant - -o optimized.png
# Optimize after converting
truss photo.jpg -o - --format webp --optimize auto | cat > optimized.webp
```

#### SVG handling
Expand Down Expand Up @@ -307,6 +312,7 @@ See the [API Reference](docs/api-reference.md) for the full endpoint list and CD
| 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) |
Expand All @@ -323,8 +329,8 @@ See the [API Reference](docs/api-reference.md) for the full endpoint list and CD
| [API Reference](docs/api-reference.md) | HTTP endpoints, request/response formats, CDN integration |
| [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 |
| [Prometheus Metrics](doc/prometheus.md) | Metrics reference, bucket boundaries, example PromQL queries |
| [OpenAPI Spec](doc/openapi.yaml) | Machine-readable API specification |
| [Prometheus Metrics](docs/prometheus.md) | Metrics reference, bucket boundaries, example PromQL queries |
| [OpenAPI Spec](docs/openapi.yaml) | Machine-readable API specification |

## Roadmap

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This page documents the HTTP API endpoints, request/response formats, and relate

## OpenAPI Specification

- OpenAPI YAML: [../doc/openapi.yaml](../doc/openapi.yaml)
- OpenAPI YAML: [openapi.yaml](openapi.yaml)
- Swagger UI on GitHub Pages: https://nao1215.github.io/truss/swagger/

## Starting the Server
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ truss is configured through environment variables and CLI flags. This page docum
| `TRUSS_PRESETS_FILE` | Path to a JSON file defining named transform presets. The file is watched for changes every 5 seconds; valid updates are applied without restart, invalid files are ignored (previous presets are kept) |
| `TRUSS_PRESETS` | Inline JSON defining named transform presets (ignored when `TRUSS_PRESETS_FILE` is set) |

Preset objects accept the same fields as the HTTP `ImageTransformOptions` schema, including `optimize` and `targetQuality`.

## S3

| Variable | Description |
Expand Down Expand Up @@ -86,7 +88,7 @@ The server exposes a `/metrics` endpoint in Prometheus text exposition format. B
| `TRUSS_DISABLE_METRICS` | Disable the `/metrics` endpoint entirely (`true`/`1`; returns 404) |
| `TRUSS_HEALTH_TOKEN` | Bearer token for `/health`; when set, requests must include `Authorization: Bearer <token>`. `/health/live` and `/health/ready` remain unauthenticated |

For the full metrics reference, bucket boundaries, and example PromQL queries, see [../doc/prometheus.md](../doc/prometheus.md).
For the full metrics reference, bucket boundaries, and example PromQL queries, see [prometheus.md](prometheus.md).

## Structured Access Logs

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The build output is written to `web/dist/`.

## Benchmark

Measured with `doc/img/logo.png` (1536 x 1024 PNG, 1.6 MB) on AMD Ryzen 7 5800U. Each operation was run 10 times; the table shows min / avg / max wall-clock time.
Measured with `docs/img/logo.png` (1536 x 1024 PNG, 1.6 MB) on AMD Ryzen 7 5800U. Each operation was run 10 times; the table shows min / avg / max wall-clock time.

### Conversion Speed

Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
54 changes: 54 additions & 0 deletions doc/openapi.yaml → docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ paths:
- $ref: '#/components/parameters/Position'
- $ref: '#/components/parameters/Format'
- $ref: '#/components/parameters/Quality'
- $ref: '#/components/parameters/Optimize'
- $ref: '#/components/parameters/TargetQuality'
- $ref: '#/components/parameters/Background'
- $ref: '#/components/parameters/Rotate'
- $ref: '#/components/parameters/AutoOrient'
Expand Down Expand Up @@ -166,6 +168,8 @@ paths:
- $ref: '#/components/parameters/Position'
- $ref: '#/components/parameters/Format'
- $ref: '#/components/parameters/Quality'
- $ref: '#/components/parameters/Optimize'
- $ref: '#/components/parameters/TargetQuality'
- $ref: '#/components/parameters/Background'
- $ref: '#/components/parameters/Rotate'
- $ref: '#/components/parameters/AutoOrient'
Expand Down Expand Up @@ -244,6 +248,8 @@ paths:
width: 1600
format: webp
quality: 82
optimize: auto
targetQuality: ssim:0.98
autoOrient: true
stripMetadata: true
responses:
Expand Down Expand Up @@ -709,6 +715,36 @@ components:
minimum: 1
maximum: 100
example: 82
Optimize:
name: optimize
in: query
required: false
description: |
Optimization mode for the final encoding stage.
`none` preserves the existing behavior.
`auto` picks a format-appropriate strategy.
`lossless` uses only pixel-preserving techniques.
`lossy` may reduce quality to save more bytes.
schema:
type: string
enum: [none, auto, lossless, lossy]
default: none
example: auto
TargetQuality:
name: targetQuality
in: query
required: false
description: |
Perceptual target used by lossy optimization, for example `ssim:0.98`
or `psnr:42`. Requires `optimize=auto` or `optimize=lossy`.
SSIM must be in `(0,1]`; PSNR must be greater than `0`.
schema:
oneOf:
- type: string
pattern: '^ssim:(?:0\.[0-9]*[1-9][0-9]*|1(?:\.0+)?)$'
- type: string
pattern: '^psnr:(?:0\.[0-9]*[1-9][0-9]*|[1-9][0-9]*(?:\.[0-9]+)?)$'
example: ssim:0.98
Background:
name: background
in: query
Expand Down Expand Up @@ -985,6 +1021,21 @@ components:
minimum: 1
maximum: 100
description: Quality for lossy outputs (`jpeg`, `webp`, `avif`).
optimize:
type: string
enum: [none, auto, lossless, lossy]
default: none
description: Optimization mode for the final encoding stage.
targetQuality:
description: >-
Perceptual target used by lossy optimization, for example
`ssim:0.98` or `psnr:42`. SSIM must be in `(0,1]`; PSNR must be
greater than `0`. Requires `optimize=auto` or `optimize=lossy`.
oneOf:
- type: string
pattern: '^ssim:(?:0\.[0-9]*[1-9][0-9]*|1(?:\.0+)?)$'
- type: string
pattern: '^psnr:(?:0\.[0-9]*[1-9][0-9]*|[1-9][0-9]*(?:\.[0-9]+)?)$'
background:
type: string
pattern: '^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$'
Expand Down Expand Up @@ -1042,6 +1093,8 @@ components:
position: center
format: webp
quality: 82
optimize: auto
targetQuality: ssim:0.98
autoOrient: true
stripMetadata: true
WatermarkPayload:
Expand Down Expand Up @@ -1098,6 +1151,7 @@ components:
width: 1600
format: webp
quality: 82
optimize: auto
UploadImageTransformRequest:
type: object
description: |
Expand Down
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions integration/cli/spec/convert_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ Describe "Convert command"
End
End

Describe "Optimization"
It "optimizes PNG losslessly via the optimize subcommand"
When run command truss optimize "$SAMPLE_PNG" -o "${WORK_DIR}/optimized.png" --mode lossless
The status should eq 0
The path "${WORK_DIR}/optimized.png" should be file
End
End

Describe "Implicit subcommand"
It "converts without the 'convert' keyword (truss <INPUT> -o <OUTPUT>)"
When run command truss "$SAMPLE_PNG" -o "${WORK_DIR}/implicit.jpg"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-wasm-demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ wasm-bindgen \
cp "$ROOT_DIR/web/index.html" "$DIST_DIR/index.html"
cp "$ROOT_DIR/web/app.js" "$DIST_DIR/app.js"
cp "$ROOT_DIR/web/styles.css" "$DIST_DIR/styles.css"
cp "$ROOT_DIR/doc/openapi.yaml" "$DIST_DIR/openapi.yaml"
cp "$ROOT_DIR/docs/openapi.yaml" "$DIST_DIR/openapi.yaml"
cp "$ROOT_DIR/web/swagger/index.html" "$SWAGGER_DIST_DIR/index.html"
cp "$ROOT_DIR/web/swagger/swagger.css" "$SWAGGER_DIST_DIR/swagger.css"
: > "$DIST_DIR/.nojekyll"
Loading
Loading