# image — raster image processing adapter

Image processing domain adapter — real raster pixel work via sharp (libvips): resize/crop/fit, format convert, compress, thumbnail, rotate/flip, grayscale/blur/sharpen/tint/negate/modulate, composite...


Real image processing for the Leumas ecosystem, powered by [`sharp`](https://sharp.pixelplumbing.com/)
(libvips). Every tool takes a single `args` object (so an HTTP POST body maps 1:1) and returns a plain
JSON-serializable result. Inputs accept a **base64 string** (raw or a `data:image/...;base64,` URI) **or
a local file path**; outputs return a **base64-encoded image** plus `format`/`width`/`height`.

Contract: `export default { metadata, adapters: { <tool>: async (args) => result } }`.

## Lazy-guarded native dependency

`sharp` is a native (libvips) module. It is imported **lazily** and **guarded**: the pack always loads,
even on a machine where sharp failed to install/compile. In that case every tool returns:

```json
{ "ok": false, "unavailable": true, "error": "sharp not installed: ..." }
```

so callers can degrade gracefully instead of the whole registry failing to load. On success each
processing tool returns `{ ok:true, base64, format, width, height, channels, size, mime }`. Pass
`dataUri:true` in args to get a ready-to-embed `data:image/...;base64,...` string instead of raw base64.

## Tools

| Tool | What it does |
|---|---|
| `resize` | Resize to exact `width`/`height` (omit one to keep aspect ratio). Optional `fit`. |
| `crop` | Extract a `width`×`height` region at `left`/`top`. |
| `resizeToFit` | Fit into a box with `fit` = `contain` (pad) / `cover` (crop) / `fill` / `inside` / `outside`. |
| `convert` | Re-encode to `format` = jpeg/png/webp/avif/tiff/gif, optional `quality`. |
| `compress` | Re-encode at a lower `quality` (default 60) to shrink size; keeps input format by default. |
| `thumbnail` | Cover-cropped thumbnail at `size` (default 128), webp by default. |
| `rotate` | Rotate by `angle` degrees (default 90); non-90° fills corners with `background`. |
| `flip` | Mirror vertically. |
| `flop` | Mirror horizontally. |
| `grayscale` | Desaturate to grayscale. |
| `blur` | Gaussian blur (`sigma`, default 3). |
| `sharpen` | Sharpen (`sigma`, default 1). |
| `tint` | Tint toward a `color` (hex or CSS name). |
| `negate` | Photographic negative (`alpha:true` also inverts alpha). |
| `modulate` | Adjust `brightness`/`saturation`/`hue`/`lightness`. |
| `gamma` | Gamma correction (`gammaValue` 1.0–3.0). |
| `normalize` | Auto-stretch contrast. |
| `extend` | Pad a border (`all` or `top`/`bottom`/`left`/`right`) with `background`. |
| `trim` | Trim uniform border pixels. |
| `extractChannel` | Extract `channel` red/green/blue/alpha (or 0-3) as grayscale. |
| `composite` | Overlay a watermark/logo (`overlay`) with `position`/`blend`/`opacity`. |
| `flatten` | Flatten transparency onto a solid `background`. |
| `metadata` | Read width/height/format/channels/space/hasAlpha/orientation without re-encoding. |
| `stats` | Per-channel min/max/mean/stdev + isOpaque/entropy/dominant. |
| `dominantColor` | Dominant colour as `{r,g,b}` + `#hex`. |
| `stripExif` | Re-encode without EXIF/metadata (bakes in orientation first). |
| `toBase64` | Normalize any accepted input to base64 (optionally a data URI / target `format`). |

## Usage

```js
import image from './index.js';

// Make a 128px webp thumbnail from a file path:
const thumb = await image.adapters.thumbnail({ image: 'C:/photos/cat.jpg', size: 128 });
// → { ok:true, base64:'...', format:'webp', width:128, height:128, ... }

// Convert an inline base64 PNG to a compressed JPEG data URI:
const jpg = await image.adapters.convert({ image: pngB64, format: 'jpeg', quality: 70, dataUri: true });

// Watermark: overlay a logo bottom-right at 50% opacity:
await image.adapters.composite({ image: photoB64, overlay: logoB64, position: 'southeast', opacity: 0.5 });

// Inspect without decoding a full re-encode:
await image.adapters.metadata({ image: 'C:/photos/cat.jpg' });
```

## DRY boundary

- **vs `a-transformation`** — a-transformation only reads an image's **basename / statInfo** (the
  filename string + `fs` stat metadata: size, mtime, path parts). That is filesystem/string work, not
  pixels. **This** `image` pack does the actual pixel decoding & processing via sharp. No overlap — a
  filename helper stays in a-transformation; anything touching pixels lives here.
- **vs `numbers`** — `numbers.baseConvert` is integer radix conversion; the base64 in this pack is
  binary image encoding. Different concerns.
- Self-contained: no cross-pack imports. `sharp` is the only external dependency (declared in the local
  `package.json` for provenance; installed at the workspace root).


---
Source: shared/engines/adapters/domain/image/README.md
Canonical: https://docs.leumas.tech/p/adapters/adapter-image
