mono/packages/media/cpp/README.md

373 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pm-image (C++)
CMake-based **`pm-image`** binary: **CLI**, **HTTP REST** (`serve`), and **line-delimited JSON IPC** (`ipc`). Image processing uses **libvips** — the same engine as **[Sharp](https://sharp.pixelplumbing.com/)** (Node.js), exposed with a similar option model. Optional **`.env`** is loaded from the working directory ([laserpants/dotenv-cpp](https://github.com/laserpants/dotenv-cpp)).
- **API reference (underlying library):** [libvips `VipsImage`](https://www.libvips.org/API/current/class.Image.html)
## Prerequisites
| Requirement | Notes |
|-------------|--------|
| CMake | ≥ 3.20 |
| C++ compiler | C++17 |
| **libvips** | **Required** — pkg-config (Unix) or `third_party/vips-dev-*` / `VIPS_ROOT` (Windows) |
| Git | For `FetchContent` (CLI11, Asio, httplib, json, **libcurl**, dotenv, [p-ranav/glob](https://github.com/p-ranav/glob), [PicoSHA2](https://github.com/okdshin/PicoSHA2) for cache keys) |
| Node.js | Optional — `npm run test:media` (Node 18+) |
### Installing libvips
**Debian / Ubuntu**
```bash
sudo apt install libvips-dev pkg-config
```
**macOS (Homebrew)**
```bash
brew install vips pkg-config
```
**Windows (official dev bundle — recommended)**
```powershell
npm run setup:vips
```
Downloads [build-win64-mxe `vips-dev-x64-all`](https://github.com/libvips/build-win64-mxe/releases) into `third_party/vips-dev-*`. CMake adds that path automatically; DLLs are copied to `dist/` on link.
Pin the version with **`MEDIA_VIPS_VERSION`** (default `8.18.2`) if needed.
Alternatively set **`VIPS_ROOT`** or **`CMAKE_PREFIX_PATH`** to a tree with `include/vips/vips.h` and `lib/libvips.lib`.
## Build
```bash
cd packages/media/cpp
cmake --preset release
cmake --build --preset release
```
Binary: **`dist/pm-image`** (`.exe` on Windows).
### Windows installer (NSIS)
From `packages/media/cpp`, after a release build:
```bash
npm run build:installer
```
Produces **`dist/pm-image-Setup.exe`**, installs `pm-image.exe`, libvips DLLs, `vips-modules-8.18`, and `scripts/explorer-resize.ps1`, and prepends the install directory to the **user** `PATH`. Uninstaller is registered under Add/Remove Programs.
### Explorer context menu (`pm-image register-explorer`)
Implemented in **C++** (Windows registry). The NSIS installer runs `pm-image.exe register-explorer` after copying files; uninstall runs `register-explorer --unregister --no-refresh-shell`.
```bash
pm-image register-explorer
pm-image register-explorer --dry
pm-image register-explorer --unregister
```
Registers a **PM-Media** cascading menu on image extensions (including **`.avif`**, **`.arw`**, **`.webp`**, TIFF, etc.), on **folders**, and on the **folder background** (empty area). **Resize** presets (default widths **1980, 1200, 800, 400**): **in place** or **copy** as `${SRC_NAME}_${width}${SRC_EXT}`. **Convert to JPG** writes `${SRC_NAME}_converted.jpg` via `explorer-convert.ps1`. Override widths with `--widths 1920,800`.
Under `SystemFileAssociations`, each extension is registered as **`.ext`**, **`.EXT`**, and **`.Ext`** so Explorer picks up the menu regardless of filename casing.
Defaults: **`--media-bin`** = this executable; **`--explorer-script`** / **`--explorer-convert-script`** = `<exe_dir>\\scripts\\explorer-*.ps1`, or `packages/media/scripts\\…` from a dev `cpp\\dist` build.
`pm-media register-explorer` forwards argv to `pm-image.exe` for convenience.
Context menus run `powershell.exe -WindowStyle Hidden`; re-run `pm-image register-explorer` after updates so registry command lines refresh.
## Formats — same idea as Sharp / libvips
Sharp wraps libvips: **decode → process → encode**. We do the same with `vips_image_new_from_file` and format-specific savers.
| | Supported |
|---|-----------|
| **Resize / geometry** | `fit`, dimensions, crop (`cover` + `position`), letterbox (`contain` + `background`), rotate, flip, flop, EXIF `autorotate` |
| **Output (first-class in code)** | **JPEG** (`Q`, strip), **PNG** (`compression`), **WebP** (`Q`, strip), **TIFF** |
| **AVIF / HEIC** | Via `format` / file extension and `quality` (libvips HEIF/AVIF saver — needs libheif in your libvips build) |
| **Anything else libvips knows** | Fallback: `vips_image_write_to_file` from extension (e.g. **GIF**, **JP2K**, … depending on how libvips was built) |
**Input** types match **whatever your libvips build can load** (the Windows `vips-dev-x64-all` bundle includes broad loader support). Set output format with the **`output` path extension** or JSON / CLI **`format`** (`webp`, `avif`, `jpg`, …).
## Sharp-like options (`resize` / JSON)
| Sharp concept | `pm-image` / JSON field | Notes |
|---------------|---------------------------|--------|
| `resize.fit` | `fit` | `inside`, `cover`, `contain`, `fill`, `outside` |
| `resize.position` | `position` | `centre`, `attention`, `entropy`, … → libvips *interesting* |
| `resize.kernel` | `kernel` | `nearest`, `cubic`, `mitchell`, `lanczos2`, `lanczos3` (default) |
| `jpeg|webp|… quality` | `quality` | 1100 |
| `png compression` | `png_compression` | 09 |
| `withoutEnlargement` | `without_enlargement` | Default **true**; CLI `--allow-enlargement` flips |
| EXIF orientation | `autorotate` | Default **true**; CLI `--no-autorotate` |
| Strip metadata | `strip_metadata` | Default **true**; CLI `--no-strip` |
| `rotate` | `rotate` | 0, 90, 180, 270 (after autorotate) |
| `flip` / `flop` | `flip` / `flop` | |
| Letterbox | `background` | `#rrggbb` for `contain` |
**Windows:** `pm-image resize --ui` opens a **native Win32** dialog to choose input/output paths, max dimensions, fit mode, quality, and enlargement / autorotate / strip options. Other CLI flags seed the dialog; **`--src` / `--dst` are not allowed** with `--ui`. If you cancel, the command exits without processing.
**REST** `POST /v1/resize` accepts **`application/json`** (same keys as the table, plus **“Batch paths & cache”** below) **or** **`multipart/form-data`** (upload a file; response is the **image bytes** — see **`serve`**). **IPC** uses the same JSON keys as the table, plus the batch/cache fields.
---
## Batch paths & cache: globs, variables, caching
Use this when **one** `resize` invocation (CLI, or a single REST/IPC request) should match **many** inputs or build **per-file** output paths. See also [docs/Examples.md](../docs/Examples.md) (TypeScript / Sharp reference).
### Summary
| Topic | What it does |
|-------|----------------|
| **Input glob** | `*` / `?` / `**` in `input` expand to a list of files ([p-ranav/glob](https://github.com/p-ranav/glob)). |
| **HTTP(S) URL** | `input` may be `http://` or `https://` — image is fetched with **libcurl** (follows redirects; `--url-timeout` default **5** s; `--url-max-redirects` default **20**). Cache key is URL + options (no local mtime). |
| **Omit `output` (CLI)** | If the input resolves to **exactly one** file or URL, you may omit the second positional argument: the file is written to the **current working directory** using a **sanitized basename** (same rules as `sanitizeFilename` in [`packages/acl`](../../acl/src/vfs/path-sanitizer.ts) — illegal/control chars, Windows reserved names, trailing dots/spaces, 255-byte UTF-8 cap). URLs without a path extension default to **`.jpg`**. `--format` overrides the output extension when set. |
| **Destination variables** | `output` may contain `${SRC_DIR}`, `${SRC_NAME}`, `${SRC_FILE_EXT}` (or `&{…}`) — expanded **per matched input**. |
| **expand_glob** | JSON `false`: treat `input` / `output` as literal paths (no glob expansion). Templates still apply if `output` contains `${SRC_` / `&{SRC_`. |
| **Output cache** | SHA-256 key from input path + size + mtime + options; default dir `<cwd>/cache/images/`. |
**Not supported in C++:** Bash **extglob** (e.g. `*.+(jpg)`). Use `*.jpg`, `**/*.jpg`, or separate runs. **Bare** `{SRC_NAME}` without `$` / `&` is not a placeholder — use `${SRC_NAME}` or `&{SRC_NAME}`.
### Input globs
- **Syntax:** `*`, `?`, and `**` for recursion. Paths are resolved from the current working directory unless absolute.
- **Multiple files → directory output:** `output` must be an **existing** directory, **or** a **new** directory given with a **trailing** `/` or `\` (parent dirs are created).
- **Single file** from a literal path or a glob that matches one file: `output` can be a full file path, or a directory (trailing sep) to keep the original filename.
**CLI:** use positional `input`/`output` or `--src` / `--dst` together. You can omit `output` only when `input` is a single path or URL (not a multi-match glob).
```bash
pm-image resize './photos/**/*.jpg' ./out/
pm-image resize --src './shots/*.png' --dst ./thumbs/
# URL → ./200.jpg under cwd (picsum path segment "200", default extension .jpg)
pm-image resize 'https://picsum.photos/200' --max-width 400 --url-timeout 30
```
**JSON:** same strings in `"input"` and `"output"`. When `expand_glob` is true (default), glob expansion runs when the pattern contains `*`, `?`, or `**`.
### Destination variables (`${SRC_*}` / `&{SRC_*}`)
Placeholders are expanded **after** inputs are resolved (glob or single file). Each output path is built from the **absolute** input file for that row.
| Placeholder | Meaning |
|-------------|---------|
| **SRC_DIR** | Parent directory of the current input (generic path, `/` separators). |
| **SRC_NAME** | Filename **stem** without extension (`photo` for `photo.JPG`). |
| **SRC_FILE_EXT** | Extension **with** leading dot (e.g. `.jpg`), or empty if none. |
Use cases: write beside each source (`${SRC_DIR}/out/${SRC_NAME}.webp`), suffix stems (`${SRC_NAME}_thumb.jpg`), or change extension via `format` / path.
```bash
pm-image resize --src ./photo.jpg --dst '${SRC_DIR}/${SRC_NAME}_medium.jpg' --max-width 800
pm-image resize --src './shots/*.jpg' --dst '${SRC_DIR}/${SRC_NAME}.webp' --max-width 1920
```
**REST / IPC** — same strings in JSON (escape quotes in shell as needed):
```bash
curl -s -X POST http://127.0.0.1:8080/v1/resize \
-H 'Content-Type: application/json' \
-d '{"input":"/data/in.png","output":"/out/${SRC_NAME}_thumb.webp","max_width":256}'
```
**Responses:** `{"ok":true}` for a single output; if more than one file is produced, `{"ok":true,"count":N,"outputs":["..."]}`.
### JSON reference (batch + cache)
| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `input` | string | required | Source path or glob. |
| `output` | string | required | File path, directory, or template with `${SRC_*}` / `&{SRC_*}`. |
| `expand_glob` | bool | `true` | If `false`, no glob expansion; paths are literal. |
| `cache` | bool | `true` (or server default from `serve` / `ipc` flags) | Enable/disable cache for this request. |
| `cache_dir` | string | empty → `<cwd>/cache/images` (or server `--cache-dir`) | Root directory for cached blobs. |
| `url_timeout_sec` | int | `5` | Total + connect timeout for HTTP(S) fetch (seconds; `0` = libcurl default). |
| `url_max_redirects` | int | `20` | Max redirects when fetching URLs. |
All **resize** options (`max_width`, `fit`, …) participate in the same JSON body.
**REST / IPC** JSON always require `"output"` in JSON (no automatic path). Multipart upload does not use `input`/`output` paths.
### Output cache
- **Default:** caching is **on**; root dir **`cache/images`** under the process **current working directory** (override with `--cache-dir` or JSON `cache_dir`).
- **Key:** **SHA-256** (PicoSHA2) over canonical input path, file size, modification time, and a stable encoding of **all** resize options — change any of these and you get a miss.
- **Storage:** `<cache_dir>/XX/<hex>` (two-letter shard).
- **Hit:** copy cached bytes to `output`; **libvips is not initialized** for that job.
- **Miss:** run resize, then **best-effort** store into cache (failure to store does not fail the request).
**CLI (`resize`):** `--no-cache`, `--cache-dir <path>`.
**`serve` / `ipc`:** same flags set **defaults** for requests that omit `cache` / `cache_dir`. Per-request JSON can still set `"cache": true` or `"cache_dir": "/path"` to override.
**Batch + cache:** glob batches run **sequentially** (one file after another); each file may hit or miss the cache independently.
### `serve` example with cache and glob
```bash
pm-image serve --host 127.0.0.1 -p 8080 --cache-dir /var/cache/pm-image
```
```bash
curl -s -X POST http://127.0.0.1:8080/v1/resize \
-H 'Content-Type: application/json' \
-d '{"input":"/data/in/*.jpg","output":"/data/out/","max_width":400,"cache":true,"cache_dir":"/var/cache/pm-image"}'
```
## Concurrency
- **HTTP `serve`**: cpp-httplib default thread pool (`CPPHTTPLIB_THREAD_POOL_COUNT` — see upstream `httplib.h`).
- **libvips**: processing is thread-safe per image; configure process-wide concurrency with `VIPS_CONCURRENCY` (or `vips_concurrency_set` in code later if needed).
## CLI examples
Paths below use Unix style; on Windows run `pm-image.exe` and use `.\` or full paths as needed.
### Help and version
```bash
pm-image --help
pm-image resize --help
pm-image -v
```
### `resize` — fit inside a box (default), write WebP / AVIF by extension
```bash
# Max 800×600, stay inside the box, Lanczos3 (default), write JPEG quality 85 (default)
pm-image resize photo.jpg out.jpg --max-width 800 --max-height 600
# Same, explicit quality
pm-image resize photo.jpg out.jpg --max-width 800 --max-height 600 -q 90
# WebP output (quality applies)
pm-image resize photo.jpg thumb.webp --max-width 400 --max-height 400 -q 82
# AVIF output (quality applies; needs HEIF/AVIF support in your libvips build)
pm-image resize photo.png out.avif --max-width 1200 --max-height 1200 -q 50
# Force output format when the path has no extension you trust
pm-image resize in.tif /tmp/out --format webp --max-width 512
```
### `resize` — square images (1:1)
Use the **same** `--max-width` and `--max-height` (that value is the square side in pixels). Pick **`--fit`**:
| `fit` | Result |
|-------|--------|
| **`cover`** | Fills the square; crops overflow (default crop: `--position centre`, or `attention` / `entropy` for smart crop). |
| **`contain`** | Full image inside the square; **letterboxing** on two sides if needed (`--background`). |
| **`fill`** | Stretches to the square (ignores aspect ratio). |
```bash
# 512×512 crop-to-square (avatars, thumbnails)
pm-image resize portrait.jpg avatar.jpg --fit cover --max-width 512 --max-height 512
# 1080×1080 WebP, smart crop on subject
pm-image resize product.png grid.webp --fit cover --max-width 1080 --max-height 1080 --position attention -q 85
# Square canvas, no crop — padded bands with a colour
pm-image resize panoramic.jpg square.jpg --fit contain --max-width 800 --max-height 800 --background '#111111'
# Exact square by stretching (rare)
pm-image resize any.jpg out.jpg --fit fill --max-width 256 --max-height 256
```
**REST / IPC JSON:** e.g. `"max_width": 512, "max_height": 512, "fit": "cover", "position": "attention"`.
### `resize` — cover (crop), contain (letterbox), rotate / flip
```bash
# Cover: fill 1200×630, crop centre (use --position attention for smart crop)
pm-image resize wide.jpg social.jpg --fit cover --max-width 1200 --max-height 630
# Contain: fit inside 800×600 canvas, letterbox with a background
pm-image resize logo.png padded.png --fit contain --max-width 800 --max-height 600 --background '#1a1a1a'
# EXIF autorotate (default), then rotate 90° CCW, vertical flip
pm-image resize img.jpg rotated.jpg --max-width 1024 --rotate 90 --flip
```
### `serve` — HTTP REST
```bash
# Default: http://127.0.0.1:8080 — GET /health, POST /v1/resize
pm-image serve --host 127.0.0.1 -p 8080
```
| Mode | `Content-Type` | Request | Response |
|------|----------------|---------|----------|
| **Upload** | `multipart/form-data` | Image part **`file`**, **`image`**, or **`upload`**; optional fields: `max_width`, `max_height`, `format`, `fit`, `quality`, … | **Binary image** (`Content-Type` matches format; default **JPEG** if `format` omitted). `Content-Disposition: inline; filename="resized…"`. |
| **Paths** | `application/json` | **`input`** and **`output`** (server-visible paths), plus resize / batch / cache fields | **JSON**`{"ok":true}` or batch `count` / `outputs` |
**Upload (multipart):** the response body is the **processed image**, not JSON. The on-disk **output cache is not used** for uploads (each request uses temporary files).
```bash
curl -s http://127.0.0.1:8080/health
curl -s -o thumb.jpg -X POST http://127.0.0.1:8080/v1/resize \
-F "file=@/path/in.png" \
-F "max_width=400" \
-F "quality=85"
```
**Paths (JSON):** same resize options as the CLI, with `input` and `output` paths (must be readable/writable by the server process).
```bash
curl -s -X POST http://127.0.0.1:8080/v1/resize \
-H 'Content-Type: application/json' \
-d '{"input":"/path/in.png","output":"/path/out.webp","max_width":400,"quality":80}'
```
Optional JSON: `"cache":false`, `"expand_glob":false`, `"cache_dir":"..."` — see **Batch paths & cache** above.
### `ipc` — one JSON line per connection (TCP; Unix socket on Linux/macOS)
```bash
pm-image ipc --host 127.0.0.1 -p 9333 --cache-dir ./cache/images
# elsewhere: send a single line, read one line back, e.g.
# {"input":"/tmp/a.jpg","output":"/tmp/b.webp","max_width":320,"format":"webp","cache":true}
```
Same JSON fields as REST (`input`, `output`, globs, `expand_glob`, `cache`, `cache_dir`, resize options).
## Tests
From `packages/media/cpp`, after **`cmake --build --preset release`** (or `npm run build:release`):
```bash
npm run test:media # full suite
npm run test:media:rest # REST only (JSON + multipart)
npm run test:media:multipart # multipart upload → image body only
npm run test:media:templates # `${SRC_*}` REST + IPC + CLI
npm run test:media:glob # recursive glob + templates (PNG under glob-in/)
npm run test:media:glob:raw # same for tests/assets/raw/**/*.arw (skips if none)
npm run test:media:ipc # IPC only
npm run test:media:url # HTTP(S) URL inputs — needs network
```
Requires a built **`dist/pm-image`** linked against **libvips** and fixture PNGs (`npm run generate:assets` if missing).
Each run overwrites **`tests/test-report-last.md`** with Markdown from **`orchestrator/reports.js`**: **host** (CPU, RAM, load), **Node process** (CPU/memory deltas, RSS), timing, plus **image rows** (fixture and response **byte sizes**, PNG **pixel dimensions** when known, and multipart/JSON **output file** sizes).
The suite covers **REST** (JSON paths and **multipart upload**), **IPC (TCP)**, optional **Unix socket** on non-Windows, **destination templates**, and **recursive glob + `${SRC_DIR}` / `${SRC_NAME}`** (`test:media:glob` — outputs under `tests/assets/glob-in/**/out/`, gitignored, for manual inspection). **`test:media:glob:raw`** runs the same glob flow against user-supplied **`.arw`** files under `tests/assets/raw/` (skipped when the folder is missing or has no ARW files).
**HTTP URL smoke tests** (downloads from **picsum.photos**, needs network):
```bash
npm run test:media:url
```
## License
See [LICENSE](LICENSE) in this directory when present.
## Testing - UI
- https://github.com/microsoft/winappdriver?tab=readme-ov-file
- https://github.com/pywinauto/pywinauto